table optimization (filtering, resizing and settings saved in user settings
This commit is contained in:
+217
-9
@@ -5412,17 +5412,145 @@ def _translated_filter_codes(
|
||||
return matches
|
||||
|
||||
|
||||
def _software_job_date_filter(column: Any, search_value: str):
|
||||
"""Support ISO/database text and the common German date formats."""
|
||||
def _parse_table_filter_number(value: Any) -> float | None:
|
||||
"""Parse a simple locale-friendly table-filter number."""
|
||||
text = str(value or "").strip().replace(" ", "").replace(",", ".")
|
||||
if not re.fullmatch(r"[-+]?\d+(?:\.\d+)?", text):
|
||||
return None
|
||||
try:
|
||||
return float(text)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _table_filter_range_parts(value: Any) -> tuple[str, str] | None:
|
||||
"""Return the two operands of a user-entered inclusive range."""
|
||||
text = str(value or "").strip()
|
||||
patterns = (
|
||||
r"^(?:von|from)\s+(.+?)\s+(?:bis|to)\s+(.+)$",
|
||||
r"^(?:zwischen|between)\s+(.+?)\s+(?:und|and)\s+(.+)$",
|
||||
r"^(.+?)\s*\.\.\s*(.+)$",
|
||||
r"^(.+?)\s+-\s+(.+)$",
|
||||
)
|
||||
for pattern in patterns:
|
||||
match = re.fullmatch(pattern, text, flags=re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1).strip(), match.group(2).strip()
|
||||
return None
|
||||
|
||||
|
||||
def _software_job_numeric_filter(column: Any, search_value: str):
|
||||
"""Build a numeric comparison/range filter, or return None for normal text."""
|
||||
value = str(search_value or "").strip()
|
||||
conditions = [_sql_text_contains(column, value)]
|
||||
for date_format in ("%d.%m.%Y", "%d.%m.%y", "%Y-%m-%d"):
|
||||
if value.lower() == "@empty":
|
||||
return column.is_(None)
|
||||
if value.lower() == "@notempty":
|
||||
return column.is_not(None)
|
||||
match = re.fullmatch(r"(<=|>=|<|>|=)\s*(-?\d+(?:[.,]\d+)?)", value)
|
||||
if match:
|
||||
expected = _parse_table_filter_number(match.group(2))
|
||||
if expected is None:
|
||||
return None
|
||||
return {
|
||||
"<": column < expected,
|
||||
"<=": column <= expected,
|
||||
">": column > expected,
|
||||
">=": column >= expected,
|
||||
"=": column == expected,
|
||||
}[match.group(1)]
|
||||
|
||||
range_values = _table_filter_range_parts(value)
|
||||
if range_values:
|
||||
first = _parse_table_filter_number(range_values[0])
|
||||
second = _parse_table_filter_number(range_values[1])
|
||||
if first is not None and second is not None:
|
||||
minimum, maximum = sorted((first, second))
|
||||
return and_(column >= minimum, column <= maximum)
|
||||
return None
|
||||
|
||||
|
||||
def _parse_table_filter_date(value: Any) -> tuple[datetime, bool] | None:
|
||||
"""Parse supported local/ISO dates and tell whether no time was supplied."""
|
||||
text = str(value or "").strip()
|
||||
formats = (
|
||||
("%d.%m.%Y %H:%M:%S", False),
|
||||
("%d.%m.%Y %H:%M", False),
|
||||
("%d.%m.%y %H:%M:%S", False),
|
||||
("%d.%m.%y %H:%M", False),
|
||||
("%Y-%m-%d %H:%M:%S", False),
|
||||
("%Y-%m-%d %H:%M", False),
|
||||
("%Y-%m-%dT%H:%M:%S", False),
|
||||
("%Y-%m-%dT%H:%M", False),
|
||||
("%d.%m.%Y", True),
|
||||
("%d.%m.%y", True),
|
||||
("%Y-%m-%d", True),
|
||||
)
|
||||
for date_format, date_only in formats:
|
||||
try:
|
||||
parsed = datetime.strptime(value, date_format)
|
||||
return datetime.strptime(text, date_format), date_only
|
||||
except ValueError:
|
||||
continue
|
||||
conditions.append(and_(column >= parsed, column < parsed + timedelta(days=1)))
|
||||
break
|
||||
return None
|
||||
|
||||
|
||||
def _software_job_date_comparison_filter(column: Any, search_value: str):
|
||||
"""Build a date comparison/range filter, or return None for normal text."""
|
||||
value = str(search_value or "").strip()
|
||||
if value.lower() == "@empty":
|
||||
return column.is_(None)
|
||||
if value.lower() == "@notempty":
|
||||
return column.is_not(None)
|
||||
match = re.fullmatch(r"(<=|>=|<|>|=)\s*(.+)", value)
|
||||
if match:
|
||||
parsed = _parse_table_filter_date(match.group(2))
|
||||
if not parsed:
|
||||
return None
|
||||
expected, date_only = parsed
|
||||
next_day = expected + timedelta(days=1)
|
||||
operator = match.group(1)
|
||||
if operator == "<":
|
||||
return column < expected
|
||||
if operator == "<=":
|
||||
return column < next_day if date_only else column <= expected
|
||||
if operator == ">":
|
||||
return column >= next_day if date_only else column > expected
|
||||
if operator == ">=":
|
||||
return column >= expected
|
||||
return and_(column >= expected, column < next_day) if date_only else column == expected
|
||||
|
||||
range_values = _table_filter_range_parts(value)
|
||||
if range_values:
|
||||
first = _parse_table_filter_date(range_values[0])
|
||||
second = _parse_table_filter_date(range_values[1])
|
||||
if first and second:
|
||||
(first_value, first_date_only), (second_value, second_date_only) = first, second
|
||||
if first_value <= second_value:
|
||||
start = first_value
|
||||
end_value, end_date_only = second_value, second_date_only
|
||||
else:
|
||||
start = second_value
|
||||
end_value, end_date_only = first_value, first_date_only
|
||||
end = end_value + timedelta(days=1) if end_date_only else end_value
|
||||
return and_(column >= start, column < end) if end_date_only else and_(column >= start, column <= end)
|
||||
return None
|
||||
|
||||
|
||||
def _software_job_date_filter(column: Any, search_value: str):
|
||||
"""Support comparisons/ranges plus the existing exact/text date search."""
|
||||
value = str(search_value or "").strip()
|
||||
advanced = _software_job_date_comparison_filter(column, value)
|
||||
if advanced is not None:
|
||||
return advanced
|
||||
|
||||
# Existing behavior intentionally remains unchanged for ordinary values.
|
||||
conditions = [_sql_text_contains(column, value)]
|
||||
parsed = _parse_table_filter_date(value)
|
||||
if parsed:
|
||||
date_value, date_only = parsed
|
||||
if date_only:
|
||||
conditions.append(and_(column >= date_value, column < date_value + timedelta(days=1)))
|
||||
else:
|
||||
conditions.append(column == date_value)
|
||||
return or_(*conditions)
|
||||
|
||||
|
||||
@@ -5457,7 +5585,10 @@ def _apply_software_job_filters(query: Any, request: Request, filters: dict[str,
|
||||
if terms:
|
||||
conditions = []
|
||||
for value in terms:
|
||||
if value.isdigit():
|
||||
advanced = _software_job_numeric_filter(SoftwareJob.id, value)
|
||||
if advanced is not None:
|
||||
conditions.append(advanced)
|
||||
elif value.isdigit():
|
||||
conditions.append(SoftwareJob.id == int(value))
|
||||
else:
|
||||
conditions.append(_sql_text_contains(SoftwareJob.id, value))
|
||||
@@ -5539,7 +5670,10 @@ def _apply_software_job_filters(query: Any, request: Request, filters: dict[str,
|
||||
if terms:
|
||||
conditions = []
|
||||
for value in terms:
|
||||
if re.fullmatch(r"[-+]?\d+", value):
|
||||
advanced = _software_job_numeric_filter(SoftwareJob.priority, value)
|
||||
if advanced is not None:
|
||||
conditions.append(advanced)
|
||||
elif re.fullmatch(r"[-+]?\d+", value):
|
||||
conditions.append(SoftwareJob.priority == int(value))
|
||||
else:
|
||||
conditions.append(_sql_text_contains(SoftwareJob.priority, value))
|
||||
@@ -7028,6 +7162,80 @@ def profile_language(request: Request, language_code: str = Form(...), next_url:
|
||||
))
|
||||
return RedirectResponse(redirect_target, status_code=303)
|
||||
|
||||
@app.get("/api/user-ui-state")
|
||||
def api_user_ui_state(request: Request, db: Session = Depends(get_db)):
|
||||
session_user = _session_user(request)
|
||||
if not session_user:
|
||||
return {"authenticated": False, "state": {}}
|
||||
user = db.query(User).filter(User.id == session_user.get("id")).first()
|
||||
if not user:
|
||||
return {"authenticated": False, "state": {}}
|
||||
state = user.ui_state if isinstance(getattr(user, "ui_state", None), dict) else {}
|
||||
return {"authenticated": True, "state": state}
|
||||
|
||||
|
||||
@app.post("/api/user-ui-state")
|
||||
def api_user_ui_state_save(
|
||||
request: Request,
|
||||
payload: dict = Body(default={}),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
session_user = _session_user(request)
|
||||
if not session_user:
|
||||
raise HTTPException(401, "Anmeldung erforderlich")
|
||||
|
||||
updates = payload.get("updates") or {}
|
||||
removals = payload.get("remove") or []
|
||||
if not isinstance(updates, dict) or not isinstance(removals, list):
|
||||
raise HTTPException(400, "Ungültiger UI-Zustand")
|
||||
if len(updates) > 500 or len(removals) > 500:
|
||||
raise HTTPException(400, "Zu viele UI-Zustände in einer Anfrage")
|
||||
|
||||
try:
|
||||
request_size = len(json.dumps(payload, ensure_ascii=False))
|
||||
except (TypeError, ValueError):
|
||||
raise HTTPException(400, "Ungültiger UI-Zustand")
|
||||
if request_size > 524288:
|
||||
raise HTTPException(413, "UI-Zustand ist zu groß")
|
||||
|
||||
normalized_updates: dict[str, Any] = {}
|
||||
for key, value in updates.items():
|
||||
key = str(key or "").strip()
|
||||
if not key or len(key) > 512:
|
||||
raise HTTPException(400, "Ungültiger UI-Zustandsschlüssel")
|
||||
normalized_updates[key] = value
|
||||
normalized_removals = []
|
||||
for key in removals:
|
||||
key = str(key or "").strip()
|
||||
if key and len(key) <= 512:
|
||||
normalized_removals.append(key)
|
||||
|
||||
user = (
|
||||
db.query(User)
|
||||
.filter(User.id == session_user.get("id"))
|
||||
.with_for_update()
|
||||
.first()
|
||||
)
|
||||
if not user:
|
||||
raise HTTPException(401, "Benutzerkonto wurde nicht gefunden")
|
||||
|
||||
state = dict(user.ui_state or {}) if isinstance(user.ui_state, dict) else {}
|
||||
for key in normalized_removals:
|
||||
state.pop(key, None)
|
||||
state.update(normalized_updates)
|
||||
|
||||
try:
|
||||
total_size = len(json.dumps(state, ensure_ascii=False))
|
||||
except (TypeError, ValueError):
|
||||
raise HTTPException(400, "Ungültiger UI-Zustand")
|
||||
if total_size > 1048576:
|
||||
raise HTTPException(413, "Gespeicherter UI-Zustand ist zu groß")
|
||||
|
||||
user.ui_state = state
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.post("/profile/browser-state")
|
||||
def profile_browser_state(
|
||||
request: Request,
|
||||
|
||||
Reference in New Issue
Block a user