generated from dopt-python/py311
prepare DB load and save
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -6,77 +6,190 @@ import datetime
|
||||
import enum
|
||||
import json
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from collections.abc import Sequence
|
||||
from pprint import pprint
|
||||
from typing import Any
|
||||
from typing import Annotated, Any
|
||||
|
||||
import babel
|
||||
from pydantic import BaseModel, ConfigDict, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator
|
||||
from PySide6.QtCore import QDate, Qt
|
||||
|
||||
|
||||
# %%
|
||||
# class FlatBaseModel(BaseModel):
|
||||
# @classmethod
|
||||
# def _unflatten_dict(cls, flat_dict: dict) -> dict:
|
||||
# """Hilfsmethode: Macht aus {'a__b': 1} wieder {'a': {'b': 1}}"""
|
||||
# result = {}
|
||||
# for key, value in flat_dict.items():
|
||||
# if "__" in key:
|
||||
# parts = key.split("__")
|
||||
# aktuell = result
|
||||
# for part in parts[:-1]:
|
||||
# if part not in aktuell or not isinstance(aktuell[part], dict):
|
||||
# aktuell[part] = {}
|
||||
# aktuell = aktuell[part]
|
||||
# aktuell[parts[-1]] = value
|
||||
# else:
|
||||
# result[key] = value
|
||||
# return result
|
||||
|
||||
# @model_validator(mode="before")
|
||||
# @classmethod
|
||||
# def __unflatten_input(cls, data: Any) -> Any:
|
||||
# """Eingangskontrolle: Verarbeitet flache DB/GUI-Daten zu Pydantic-Strukturen."""
|
||||
# if not isinstance(data, dict):
|
||||
# return data
|
||||
|
||||
# # 1. Haupt-Struktur wiederherstellen
|
||||
# unflattened = cls._unflatten_dict(data)
|
||||
|
||||
# print("\n----------------------")
|
||||
# pprint(unflattened)
|
||||
|
||||
# # 2. Schleife über die Felder, um JSON-Listen von Sub-Modellen zu entpacken
|
||||
# for key, value in unflattened.items():
|
||||
# print(f"{value=}")
|
||||
# if isinstance(value, str) and value.startswith("[") and value.endswith("]"):
|
||||
# try:
|
||||
# parsed = json.loads(value)
|
||||
# if isinstance(parsed, list):
|
||||
# print(f"Key {key}: identified list")
|
||||
# # Falls die Liste Dictionaries enthält, ent-flachen wir diese einzeln
|
||||
# unflattened[key] = [
|
||||
# cls._unflatten_dict(item) if isinstance(item, dict) else item
|
||||
# for item in parsed
|
||||
# ]
|
||||
# else:
|
||||
# unflattened[key] = parsed
|
||||
# except json.JSONDecodeError:
|
||||
# print(f"Key {key}: JSON serialize error")
|
||||
# pass
|
||||
# if isinstance(value, list):
|
||||
# unflattened[key] = [
|
||||
# cls._unflatten_dict(item) if isinstance(item, dict) else item
|
||||
# for item in value
|
||||
# ]
|
||||
|
||||
# print("\n\n##################Result:")
|
||||
# pprint(unflattened)
|
||||
|
||||
# return unflattened
|
||||
|
||||
# def to_db(self) -> dict[str, Any]:
|
||||
# """Ausgang für die DB: Flach, Listen sind JSON-Strings."""
|
||||
# nested = super().model_dump()
|
||||
# return self.__flatten_dict(nested, serialize_lists=True)
|
||||
|
||||
# def to_gui(self) -> dict[str, Any]:
|
||||
# """Ausgang für die GUI: Flach, aber Listen bleiben Python-Listen für Widgets."""
|
||||
# nested = super().model_dump()
|
||||
# return self.__flatten_dict(nested, serialize_lists=False)
|
||||
|
||||
# @classmethod
|
||||
# def __flatten_dict(
|
||||
# cls, nested_dict: dict, parent_key: str = "", serialize_lists: bool = True
|
||||
# ) -> dict:
|
||||
# """Rekursiver Alleskönner zum Abflachen von Strukturen."""
|
||||
# items = []
|
||||
# for k, v in nested_dict.items():
|
||||
# new_key = f"{parent_key}__{k}" if parent_key else k
|
||||
|
||||
# if isinstance(v, dict):
|
||||
# items.extend(cls.__flatten_dict(v, new_key, serialize_lists).items())
|
||||
# elif isinstance(v, list):
|
||||
# processed_list = []
|
||||
# for item in v:
|
||||
# if isinstance(item, dict):
|
||||
# # WICHTIG: Das Dict in der Liste wird isoliert abgeflacht (ohne parent_key),
|
||||
# # da es ein eigenständiges Zeilen-Objekt im DynamicListWidget bleibt!
|
||||
# processed_list.append(
|
||||
# cls.__flatten_dict(item, serialize_lists=serialize_lists)
|
||||
# )
|
||||
# else:
|
||||
# processed_list.append(item)
|
||||
|
||||
# if serialize_lists:
|
||||
# items.append((new_key, json.dumps(processed_list)))
|
||||
# else:
|
||||
# items.append((new_key, processed_list))
|
||||
# else:
|
||||
# items.append((new_key, v))
|
||||
# return dict(items)
|
||||
|
||||
|
||||
class FlatBaseModel(BaseModel):
|
||||
"""
|
||||
Optimierte Pydantic-Basisklasse, die JSON-Strings und Doppel-Unterstriche
|
||||
vollständig rekursiv (tiefenunabhängig) auflöst.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _unflatten_dict(cls, flat_dict: dict) -> dict:
|
||||
"""Hilfsmethode: Macht aus {'a__b': 1} wieder {'a': {'b': 1}}"""
|
||||
result = {}
|
||||
for key, value in flat_dict.items():
|
||||
if "__" in key:
|
||||
parts = key.split("__")
|
||||
aktuell = result
|
||||
for part in parts[:-1]:
|
||||
if part not in aktuell or not isinstance(aktuell[part], dict):
|
||||
aktuell[part] = {}
|
||||
aktuell = aktuell[part]
|
||||
aktuell[parts[-1]] = value
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
def _recursive_parse_json(cls, data: Any) -> Any:
|
||||
"""Sucht im gesamten Objekt nach JSON-Listen-Strings und entpackt sie."""
|
||||
if isinstance(data, str) and data.startswith("[") and data.endswith("]"):
|
||||
try:
|
||||
parsed = json.loads(data)
|
||||
# Falls die Liste selbst wieder konvertiert werden muss (z.B. Sub-Dicts)
|
||||
return cls._recursive_parse_json(parsed)
|
||||
except json.JSONDecodeError:
|
||||
return data
|
||||
elif isinstance(data, dict):
|
||||
return {k: cls._recursive_parse_json(v) for k, v in data.items()}
|
||||
elif isinstance(data, list):
|
||||
return [cls._recursive_parse_json(item) for item in data]
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def _recursive_unflatten(cls, data: Any) -> Any:
|
||||
"""Baut im gesamten Objekt flache '__'-Schlüssel in geschachtelte Dicts um."""
|
||||
if isinstance(data, dict):
|
||||
# 1. Die aktuelle Ebene dieses Dictionaries ent-flachen
|
||||
unflattened_level = {}
|
||||
for key, value in data.items():
|
||||
if "__" in key:
|
||||
parts = key.split("__")
|
||||
aktuell = unflattened_level
|
||||
for part in parts[:-1]:
|
||||
if part not in aktuell or not isinstance(aktuell[part], dict):
|
||||
aktuell[part] = {}
|
||||
aktuell = aktuell[part]
|
||||
aktuell[parts[-1]] = value
|
||||
else:
|
||||
unflattened_level[key] = value
|
||||
|
||||
# 2. Jetzt tiefer in die Werte gehen (rekursiv für Unter-Dicts)
|
||||
return {k: cls._recursive_unflatten(v) for k, v in unflattened_level.items()}
|
||||
|
||||
elif isinstance(data, list):
|
||||
# Auch durch Listen (z.B. deine Schulbildung) wandern und Sub-Dicts ent-flachen
|
||||
return [cls._recursive_unflatten(item) for item in data]
|
||||
|
||||
return data
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def __unflatten_input(cls, data: Any) -> Any:
|
||||
"""Eingangskontrolle: Verarbeitet flache DB/GUI-Daten zu Pydantic-Strukturen."""
|
||||
"""Eingangskontrolle: Bereitet flache DB/GUI-Daten sauber für Pydantic vor."""
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
# 1. Haupt-Struktur wiederherstellen
|
||||
unflattened = cls._unflatten_dict(data)
|
||||
pprint(unflattened)
|
||||
# Schritt 1: Alle JSON-Strings (egal wie tief) in echte Listen umwandeln
|
||||
# Das verwandelt '[1, 2, 3]' zuverlässig in [1, 2, 3]
|
||||
json_parsed_data = cls._recursive_parse_json(data)
|
||||
|
||||
# 2. Schleife über die Felder, um JSON-Listen von Sub-Modellen zu entpacken
|
||||
for key, value in unflattened.items():
|
||||
if isinstance(value, str) and value.startswith("[") and value.endswith("]"):
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
if isinstance(parsed, list):
|
||||
# Falls die Liste Dictionaries enthält, ent-flachen wir diese einzeln
|
||||
unflattened[key] = [
|
||||
cls._unflatten_dict(item) if isinstance(item, dict) else item
|
||||
for item in parsed
|
||||
]
|
||||
else:
|
||||
unflattened[key] = parsed
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
if isinstance(value, (list)):
|
||||
unflattened[key] = [
|
||||
cls._unflatten_dict(item) if isinstance(item, dict) else item
|
||||
for item in value
|
||||
]
|
||||
# Schritt 2: Alle '__' Schlüssel (egal wie tief) in Unter-Strukturen falten
|
||||
final_nested_data = cls._recursive_unflatten(json_parsed_data)
|
||||
|
||||
pprint(unflattened)
|
||||
return final_nested_data
|
||||
|
||||
return unflattened
|
||||
|
||||
def to_db(self) -> dict[str, Any]:
|
||||
def to_db(self) -> Dict[str, Any]:
|
||||
"""Ausgang für die DB: Flach, Listen sind JSON-Strings."""
|
||||
nested = super().model_dump()
|
||||
return self.__flatten_dict(nested, serialize_lists=True)
|
||||
|
||||
def to_gui(self) -> dict[str, Any]:
|
||||
"""Ausgang für die GUI: Flach, aber Listen bleiben Python-Listen für Widgets."""
|
||||
def to_gui(self) -> Dict[str, Any]:
|
||||
"""Ausgang für die GUI: Flach, aber Listen bleiben Python-Listen."""
|
||||
nested = super().model_dump()
|
||||
return self.__flatten_dict(nested, serialize_lists=False)
|
||||
|
||||
@@ -84,7 +197,7 @@ class FlatBaseModel(BaseModel):
|
||||
def __flatten_dict(
|
||||
cls, nested_dict: dict, parent_key: str = "", serialize_lists: bool = True
|
||||
) -> dict:
|
||||
"""Rekursiver Alleskönner zum Abflachen von Strukturen."""
|
||||
"""Rekursiver Helfer zum Abflachen von Strukturen."""
|
||||
items = []
|
||||
for k, v in nested_dict.items():
|
||||
new_key = f"{parent_key}__{k}" if parent_key else k
|
||||
@@ -95,8 +208,6 @@ class FlatBaseModel(BaseModel):
|
||||
processed_list = []
|
||||
for item in v:
|
||||
if isinstance(item, dict):
|
||||
# WICHTIG: Das Dict in der Liste wird isoliert abgeflacht (ohne parent_key),
|
||||
# da es ein eigenständiges Zeilen-Objekt im DynamicListWidget bleibt!
|
||||
processed_list.append(
|
||||
cls.__flatten_dict(item, serialize_lists=serialize_lists)
|
||||
)
|
||||
@@ -139,6 +250,7 @@ class ProjektModell(FlatBaseModel):
|
||||
|
||||
class Grunderfassung_Unternehmen(FlatBaseModel):
|
||||
Schulbildung: list[Grunderfassung_Schulbildung]
|
||||
Stammdaten: Grunderfassung_Stammdaten
|
||||
|
||||
|
||||
class Grunderfassung_Schulbildung(BaseModel):
|
||||
@@ -153,30 +265,90 @@ class Grunderfassung_Schulbildung(BaseModel):
|
||||
SB_bemerkungsfeld: str | None
|
||||
|
||||
|
||||
# %%
|
||||
list_schulbildung = [
|
||||
{
|
||||
"SB_abschluss": None,
|
||||
"SB_abschlussgrad": None,
|
||||
"SB_abschlussjahr": None,
|
||||
"SB_bemerkungsfeld": None,
|
||||
"SB_land": None,
|
||||
"SB_ort": None,
|
||||
"SB_schule": None,
|
||||
},
|
||||
{
|
||||
"SB_abschluss": None,
|
||||
"SB_abschlussgrad": None,
|
||||
"SB_abschlussjahr": None,
|
||||
"SB_bemerkungsfeld": None,
|
||||
"SB_land": None,
|
||||
"SB_ort": None,
|
||||
"SB_schule": None,
|
||||
},
|
||||
]
|
||||
data = {"Schulbildung": list_schulbildung}
|
||||
ValidAge = Annotated[int, Field(ge=0, le=99)]
|
||||
|
||||
Grunderfassung_Unternehmen(**data)
|
||||
|
||||
class Grunderfassung_Stammdaten(BaseModel):
|
||||
# Stammdaten_titel: str | None
|
||||
# Stammdaten_anrede_anschrift: str
|
||||
# Stammdaten_name: str
|
||||
# Stammdaten_vorname: str | None
|
||||
# Stammdaten_geburtsdatum: datetime.date | None
|
||||
# Stammdaten_herkunftsland: str
|
||||
# Stammdaten_staatsangehoerigkeit: str | None
|
||||
# Stammdaten_rueckkehrer: bool | None
|
||||
# Stammdaten_aufenthaltsort: str | None
|
||||
# Stammdaten_strasse: str | None
|
||||
# Stammdaten_hausnummer: str | None
|
||||
# Stammdaten_PLZ: str | None
|
||||
# Stammdaten_ort: str | None
|
||||
# Stammdaten_bundesland: str | None
|
||||
# Stammdaten_land: str | None
|
||||
# Stammdaten_festnetznummer: str | None
|
||||
# Stammdaten_mobilfunknummer: str | None
|
||||
# Stammdaten_email: EmailStr | None
|
||||
# Stammdaten_familienstand: str | None
|
||||
anzahl_kinder: Grunderfassung_Stammdaten_AnzahlKinder
|
||||
|
||||
# @field_validator("Stammdaten_rueckkehrer", mode="before")
|
||||
# @classmethod
|
||||
# def str_to_bool(cls, value: Any) -> Any:
|
||||
# if isinstance(value, str):
|
||||
# value = value.strip().lower()
|
||||
|
||||
# if value == "ja":
|
||||
# return True
|
||||
# elif value == "nein":
|
||||
# return False
|
||||
|
||||
# raise ValueError("Wert muss 'ja', 'nein', True oder False sein.")
|
||||
|
||||
# return value
|
||||
|
||||
|
||||
class Grunderfassung_Stammdaten_AnzahlKinder(BaseModel):
|
||||
anzahl: int | None
|
||||
alter: list[ValidAge | None] | None = None
|
||||
|
||||
|
||||
# %%
|
||||
|
||||
data = {
|
||||
"Schulbildung": [
|
||||
{
|
||||
"SB_abschluss": None,
|
||||
"SB_abschlussgrad": None,
|
||||
"SB_abschlussjahr": None,
|
||||
"SB_bemerkungsfeld": None,
|
||||
"SB_land": None,
|
||||
"SB_ort": None,
|
||||
"SB_schule": None,
|
||||
},
|
||||
{
|
||||
"SB_abschluss": None,
|
||||
"SB_abschlussgrad": None,
|
||||
"SB_abschlussjahr": None,
|
||||
"SB_bemerkungsfeld": None,
|
||||
"SB_land": None,
|
||||
"SB_ort": None,
|
||||
"SB_schule": None,
|
||||
},
|
||||
],
|
||||
"Stammdaten__anzahl_kinder__anzahl": 3,
|
||||
"Stammdaten__anzahl_kinder__alter": [1, 2, 3],
|
||||
}
|
||||
|
||||
parsed = Grunderfassung_Unternehmen(**data)
|
||||
# %%
|
||||
db_entry = parsed.to_db()
|
||||
pprint(db_entry)
|
||||
# %%
|
||||
loaded_from_db = Grunderfassung_Unternehmen(**db_entry)
|
||||
# %%
|
||||
loaded_from_db
|
||||
|
||||
# %%
|
||||
loaded_from_db.to_gui()
|
||||
|
||||
# %%
|
||||
projekt = ProjektModell(**gui_rohdaten)
|
||||
@@ -285,6 +457,48 @@ pprint(unflatted)
|
||||
flatted
|
||||
|
||||
|
||||
# %%
|
||||
# key merger
|
||||
def merge_dicts_to_lists(dict_iter):
|
||||
merged = defaultdict(list)
|
||||
|
||||
for d in dict_iter:
|
||||
for key, value in d.items():
|
||||
merged[key].append(value)
|
||||
|
||||
return dict(merged)
|
||||
|
||||
|
||||
def unmerge_dict_to_list(merged_dict):
|
||||
if not merged_dict:
|
||||
return []
|
||||
|
||||
# 1. Wir trennen die Keys und die dazugehörigen Wert-Listen
|
||||
keys = merged_dict.keys()
|
||||
value_lists = merged_dict.values()
|
||||
|
||||
# 2. zip(*value_lists) nimmt das erste Element aus jeder Liste, dann das zweite, etc.
|
||||
# 3. zip(keys, row) verbindet die Keys wieder mit den jeweiligen Werten einer Zeile
|
||||
return [dict(zip(keys, row)) for row in zip(*value_lists)]
|
||||
|
||||
|
||||
data = [
|
||||
{"Stammdaten__anzahl_kinder__alter": None},
|
||||
{"Stammdaten__anzahl_kinder__alter": 2},
|
||||
{"Stammdaten__anzahl_kinder__alter": 3},
|
||||
{"Stammdaten__anzahl_kinder__alter": None},
|
||||
]
|
||||
merged = merge_dicts_to_lists(data)
|
||||
pprint(merged)
|
||||
|
||||
merged = {"Stammdaten__anzahl_kinder__alter": [1, 2]}
|
||||
|
||||
unmerged = unmerge_dict_to_list(merged)
|
||||
pprint(unmerged)
|
||||
|
||||
# %%
|
||||
unmerge_dict_to_list({})
|
||||
|
||||
# %%
|
||||
string = """
|
||||
Metallerzeugung & -bearbeitung; Elektro, Energie, Chemie; IT & Software; Kunststoff, Papier, Textil; Logistik, Verkehr, Transport; Handwerk, Bau, Grüne Berufe; Gesundheit & Pflege; Tourismus & Gastronomie; Handel; Bildung & Soziales; Entwicklung, Planung, Qualität; Administration, Finanzen, Verwaltung; Marketing, Design, Vertrieb; Einkauf, Lager, Wartung; Sonstige; Keine Schwerpunkte, branchenübergreifende Rekrutierung
|
||||
|
||||
@@ -33,6 +33,7 @@ class SafeDateTime(TypeDecorator):
|
||||
|
||||
|
||||
md_crm = sql.MetaData()
|
||||
md_main = sql.MetaData()
|
||||
|
||||
# ---------- OLD "Kontaktliste" ----------
|
||||
|
||||
@@ -295,3 +296,58 @@ def get_ext_crm_contact_person(
|
||||
|
||||
|
||||
df_contact_person = get_ext_crm_contact_person(None)
|
||||
|
||||
|
||||
grunderfassung_unternehmen: sql.Table = Table(
|
||||
"grunderfassung_unternehmen",
|
||||
md_main,
|
||||
Column("erfassung_id", sql.Integer, nullable=False, unique=True, autoincrement=True),
|
||||
Column("Arbeitserfahrung", sql.Text, nullable=True),
|
||||
Column("Grunderfassung_fallnummer", sql.Text, nullable=True),
|
||||
Column("Grunderfassung_notiz", sql.Text, nullable=True),
|
||||
Column("HoehereBildung", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_adresse", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_anrede_anschrift", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_email", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_festnetznummer", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_funktion_beziehung", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_mobilfunknummer", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_name", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_name_partner", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_titel", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_vorname", sql.Text, nullable=True),
|
||||
Column("Metadaten_aktualisierung", sql.Text, nullable=True),
|
||||
Column("Metadaten_erstellung", sql.Text, nullable=True),
|
||||
Column("Metadaten_nutzer", sql.String(20), nullable=True),
|
||||
Column("Partnersuche__kanal_aufmerksamkeit", sql.Text, nullable=True),
|
||||
Column("Partnersuche__person_suche", sql.Text, nullable=True),
|
||||
Column("Partnersuche__un_suche", sql.Text, nullable=True),
|
||||
Column("Projektrelevanz__relevanz", sql.Boolean, nullable=True),
|
||||
Column("Schulbildung", sql.Text, nullable=True),
|
||||
Column("Sprachkenntnisse", sql.Text, nullable=True),
|
||||
Column("Stammdaten__PLZ", sql.Text, nullable=True),
|
||||
Column("Stammdaten__anrede_anschrift", sql.Text, nullable=True),
|
||||
Column("Stammdaten__anzahl_kinder__alter", sql.Text, nullable=True),
|
||||
Column("Stammdaten__anzahl_kinder__anzahl", sql.Text, nullable=True),
|
||||
Column("Stammdaten__aufenthaltsort", sql.Text, nullable=True),
|
||||
Column("Stammdaten__bundesland", sql.Text, nullable=True),
|
||||
Column("Stammdaten__email", sql.Text, nullable=True),
|
||||
Column("Stammdaten__familienstand", sql.Text, nullable=True),
|
||||
Column("Stammdaten__festnetznummer", sql.Text, nullable=True),
|
||||
Column("Stammdaten__geburtsdatum", sql.Text, nullable=True),
|
||||
Column("Stammdaten__hausnummer", sql.Text, nullable=True),
|
||||
Column("Stammdaten__herkunftsland", sql.Text, nullable=True),
|
||||
Column("Stammdaten__mobilfunknummer", sql.Text, nullable=True),
|
||||
Column("Stammdaten__name", sql.Text, nullable=True),
|
||||
Column("Stammdaten__ort", sql.Text, nullable=True),
|
||||
Column("Stammdaten__rueckkehrer", sql.Boolean, nullable=True),
|
||||
Column("Stammdaten__staatsangehoerigkeit", sql.Text, nullable=True),
|
||||
Column("Stammdaten__strasse", sql.Text, nullable=True),
|
||||
Column("Stammdaten__titel", sql.Text, nullable=True),
|
||||
Column("Stammdaten__vorname", sql.Text, nullable=True),
|
||||
Column("WeitereInfos__WI_arbeitsstatus", sql.Text, nullable=True),
|
||||
Column("WeitereInfos__WI_aufenthaltstitel", sql.Text, nullable=True),
|
||||
Column("WeitereInfos__WI_deutsch_sprache", sql.Text, nullable=True),
|
||||
Column("WeitereInfos__WI_gueltigkeit_aufenthaltstitel", sql.Text, nullable=True),
|
||||
Column("WeitereInfos__WI_meldung_institution", sql.Text, nullable=True),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user