mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
typing
This commit is contained in:
@@ -662,7 +662,7 @@ class IfcImporter:
|
||||
settings: Optional[ifcopenshell.geom.main.settings] = None,
|
||||
) -> set[ifcopenshell.entity_instance]:
|
||||
checkpoint = time.time()
|
||||
results = set()
|
||||
results: set[ifcopenshell.entity_instance] = set()
|
||||
if not products:
|
||||
return results
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ from bpy.props import (
|
||||
from typing import Union, TYPE_CHECKING, Literal
|
||||
|
||||
|
||||
def get_active_dictionary(self, context):
|
||||
def get_active_dictionary(self: "BIMBSDDProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
if not BSDDData.is_loaded:
|
||||
BSDDData.load()
|
||||
return BSDDData.data["active_dictionary"]
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# pyright: reportAttributeAccessIssue=false
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
|
||||
@@ -36,24 +36,25 @@ class ExecuteIfcFM(bpy.types.Operator, ExportHelper):
|
||||
|
||||
@property
|
||||
def filename_ext(self) -> str:
|
||||
props = bpy.context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
return f".{props.format}"
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
layout = self.layout
|
||||
props = context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
layout.prop(props, "format")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
if not props.should_load_from_memory and not props.ifc_files.single_file:
|
||||
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
ifc_file = tool.Ifc.get()
|
||||
filepaths = []
|
||||
if ifc_file and props.should_load_from_memory:
|
||||
@@ -95,7 +96,7 @@ class SelectFMSpreadsheetFiles(bpy.types.Operator, ImportHelper):
|
||||
files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement)
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
props.spreadsheet_files.clear()
|
||||
dirname = os.path.dirname(self.filepath)
|
||||
for f in self.files:
|
||||
@@ -112,24 +113,24 @@ class ExecuteIfcFMFederate(bpy.types.Operator, ExportHelper):
|
||||
|
||||
@property
|
||||
def filename_ext(self) -> str:
|
||||
props = bpy.context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
return f".{props.format}"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
layout.prop(props, "format")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
if not props.spreadsheet_files:
|
||||
cls.poll_message_set("No spreadsheet files selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
parser = ifcfm.Parser(preset=props.engine)
|
||||
parser.federate([f.name for f in props.spreadsheet_files])
|
||||
writer = ifcfm.Writer(parser)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.fm.data import FMData
|
||||
from bonsai.bim.prop import MultipleFileSelect, StrProperty
|
||||
from bpy.types import PropertyGroup
|
||||
@@ -30,9 +31,10 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
|
||||
def get_engine(self, context):
|
||||
def get_engine(self: "BIMFMProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
if not FMData.is_loaded:
|
||||
FMData.load()
|
||||
return FMData.data["engine"]
|
||||
@@ -57,3 +59,10 @@ class BIMFMProperties(PropertyGroup):
|
||||
default="ods",
|
||||
options=set(),
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
ifc_files: MultipleFileSelect
|
||||
spreadsheet_files: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
should_load_from_memory: bool
|
||||
engine: str
|
||||
format: Literal["csv", "xlsx", "ods"]
|
||||
|
||||
@@ -33,11 +33,11 @@ class BIM_PT_fm(Panel):
|
||||
if not FMData.is_loaded:
|
||||
FMData.load()
|
||||
|
||||
assert self.layout
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
|
||||
scene = context.scene
|
||||
props = scene.BIMFMProperties
|
||||
props = tool.Blender.get_fm_props()
|
||||
|
||||
if tool.Ifc.get():
|
||||
row = layout.row()
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# pyright: reportAttributeAccessIssue=false
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
|
||||
@@ -367,9 +367,10 @@ class WorkPlansData:
|
||||
@classmethod
|
||||
def active_work_plan_schedules(cls):
|
||||
results = []
|
||||
if not bpy.context.scene.BIMWorkPlanProperties.active_work_plan_id:
|
||||
props = tool.Sequence.get_work_plan_props()
|
||||
if not props.active_work_plan_id:
|
||||
return []
|
||||
for rel in tool.Ifc.get().by_id(bpy.context.scene.BIMWorkPlanProperties.active_work_plan_id).IsDecomposedBy:
|
||||
for rel in tool.Ifc.get().by_id(props.active_work_plan_id).IsDecomposedBy:
|
||||
for work_schedule in rel.RelatedObjects:
|
||||
results.append({"id": work_schedule.id(), "name": work_schedule.Name or "Unnamed"})
|
||||
return results
|
||||
|
||||
@@ -140,10 +140,11 @@ class EditWorkPlan(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Edit Work Plan"
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Sequence.get_work_plan_props()
|
||||
core.edit_work_plan(
|
||||
tool.Ifc,
|
||||
tool.Sequence,
|
||||
work_plan=tool.Ifc.get().by_id(context.scene.BIMWorkPlanProperties.active_work_plan_id),
|
||||
work_plan=tool.Ifc.get().by_id(props.active_work_plan_id),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -88,7 +88,8 @@ class BIM_PT_work_plans(Panel):
|
||||
def draw(self, context):
|
||||
if not WorkPlansData.is_loaded:
|
||||
WorkPlansData.load()
|
||||
self.props = context.scene.BIMWorkPlanProperties
|
||||
assert self.layout
|
||||
self.props = tool.Sequence.get_work_plan_props()
|
||||
|
||||
row = self.layout.row()
|
||||
if WorkPlansData.data["total_work_plans"]:
|
||||
@@ -99,7 +100,7 @@ class BIM_PT_work_plans(Panel):
|
||||
for work_plan in WorkPlansData.data["work_plans"]:
|
||||
self.draw_work_plan_ui(work_plan)
|
||||
|
||||
def draw_work_plan_ui(self, work_plan):
|
||||
def draw_work_plan_ui(self, work_plan: dict[str, Any]) -> None:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=work_plan["name"], icon="TEXT")
|
||||
if self.props.active_work_plan_id == work_plan["id"]:
|
||||
@@ -121,10 +122,10 @@ class BIM_PT_work_plans(Panel):
|
||||
elif self.props.editing_type == "SCHEDULES":
|
||||
self.draw_work_schedule_ui()
|
||||
|
||||
def draw_editable_ui(self):
|
||||
def draw_editable_ui(self) -> None:
|
||||
draw_attributes(self.props.work_plan_attributes, self.layout)
|
||||
|
||||
def draw_work_schedule_ui(self):
|
||||
def draw_work_schedule_ui(self) -> None:
|
||||
if WorkPlansData.data["has_work_schedules"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "work_schedules", text="")
|
||||
@@ -177,7 +178,8 @@ class BIM_PT_work_schedules(Panel):
|
||||
for work_schedule_id, work_schedule in SequenceData.data["work_schedules"].items():
|
||||
self.draw_work_schedule_ui(work_schedule_id, work_schedule)
|
||||
|
||||
def draw_work_schedule_ui(self, work_schedule_id, work_schedule):
|
||||
def draw_work_schedule_ui(self, work_schedule_id: int, work_schedule: dict[str, Any]) -> None:
|
||||
assert self.layout
|
||||
if work_schedule["PredefinedType"] == "BASELINE":
|
||||
self.draw_readonly_work_schedule_ui(work_schedule_id)
|
||||
else:
|
||||
@@ -250,7 +252,7 @@ class BIM_PT_work_schedules(Panel):
|
||||
self.draw_column_ui()
|
||||
self.draw_editable_task_ui(work_schedule_id)
|
||||
|
||||
def draw_task_operators(self):
|
||||
def draw_task_operators(self) -> None:
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
ifc_definition_id = None
|
||||
@@ -285,9 +287,10 @@ class BIM_PT_work_schedules(Panel):
|
||||
row.operator("bim.duplicate_task", text="Copy", icon="DUPLICATE").task = ifc_definition_id
|
||||
row.operator("bim.remove_task", text="Delete", icon="X").task = ifc_definition_id
|
||||
|
||||
def draw_column_ui(self):
|
||||
def draw_column_ui(self) -> None:
|
||||
if not self.props.should_show_column_ui:
|
||||
return
|
||||
assert self.layout
|
||||
row = self.layout.row()
|
||||
row.operator("bim.setup_default_task_columns", text="Setup Default Columns", icon="ANCHOR_BOTTOM")
|
||||
row.alignment = "RIGHT"
|
||||
@@ -318,7 +321,8 @@ class BIM_PT_work_schedules(Panel):
|
||||
def draw_editable_work_schedule_ui(self):
|
||||
draw_attributes(self.props.work_schedule_attributes, self.layout)
|
||||
|
||||
def draw_editable_task_ui(self, work_schedule_id):
|
||||
def draw_editable_task_ui(self, work_schedule_id: int) -> None:
|
||||
assert self.layout
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Task Tools")
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
@@ -47,7 +47,7 @@ from collections.abc import Iterable
|
||||
from natsort import natsorted
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.prop import MultipleFileSelect
|
||||
from bonsai.bim.prop import MultipleFileSelect, Attribute
|
||||
|
||||
|
||||
class SetTab(bpy.types.Operator):
|
||||
@@ -1343,7 +1343,7 @@ class BIM_OT_attribute_search_values(bpy.types.Operator):
|
||||
collection_values: bpy.props.CollectionProperty(type=StrProperty, options={"SKIP_SAVE"})
|
||||
|
||||
@staticmethod
|
||||
def resolve_data_path(data_path: str) -> tuple[str, object]:
|
||||
def resolve_data_path(data_path: str) -> tuple[str, "Attribute"]:
|
||||
"""Resolve the data path of an object's attribute to get the attribute name and the object."""
|
||||
path_parts = data_path.split(".")
|
||||
obj_path = ".".join(path_parts[:-1])
|
||||
@@ -1361,11 +1361,12 @@ class BIM_OT_attribute_search_values(bpy.types.Operator):
|
||||
for value in string_values:
|
||||
self.collection_values.add().name = value
|
||||
|
||||
assert context.window_manager
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def get_unique_attribute_values(self):
|
||||
def get_unique_attribute_values(self) -> list[str]:
|
||||
ifc_file = tool.Ifc.get()
|
||||
unique_values = set()
|
||||
unique_values: set[str] = set()
|
||||
ifc_class = self.attribute_ifc_class
|
||||
|
||||
elements = ifc_file.by_type(ifc_class, include_subtypes=True)
|
||||
@@ -1379,7 +1380,8 @@ class BIM_OT_attribute_search_values(bpy.types.Operator):
|
||||
|
||||
return list(unique_values)
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context) -> None:
|
||||
assert self.layout
|
||||
row = self.layout.row()
|
||||
row.label(text=f"Select {self.attribute_name} value:")
|
||||
row = self.layout.row()
|
||||
|
||||
@@ -61,6 +61,7 @@ if TYPE_CHECKING:
|
||||
from bonsai.bim.module.constraint.prop import BIMConstraintProperties, BIMObjectConstraintProperties
|
||||
from bonsai.bim.module.csv.prop import CsvProperties
|
||||
from bonsai.bim.module.diff.prop import DiffProperties
|
||||
from bonsai.bim.module.fm.prop import BIMFMProperties
|
||||
from bonsai.bim.module.group.prop import BIMGroupProperties
|
||||
from bonsai.bim.module.light.prop import BIMSolarProperties, RadianceExporterProperties
|
||||
|
||||
@@ -1663,51 +1664,56 @@ class Blender(bonsai.core.tool.Blender):
|
||||
|
||||
@classmethod
|
||||
def get_object_constraint_props(cls, obj: bpy.types.Object) -> BIMObjectConstraintProperties:
|
||||
return obj.BIMObjectConstraintProperties
|
||||
return obj.BIMObjectConstraintProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_constraint_props(cls) -> BIMConstraintProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMConstraintProperties
|
||||
return scene.BIMConstraintProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_csv_props(cls) -> CsvProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.CsvProperties
|
||||
return scene.CsvProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_diff_props(cls) -> DiffProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.DiffProperties
|
||||
return scene.DiffProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_group_props(cls) -> BIMGroupProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMGroupProperties
|
||||
return scene.BIMGroupProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_bim_props(cls, scene: Optional[bpy.types.Scene] = None) -> BIMProperties:
|
||||
if scene is None:
|
||||
scene = bpy.context.scene
|
||||
return scene.BIMProperties
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_object_bim_props(cls, obj: bpy.types.Object) -> BIMObjectProperties:
|
||||
return obj.BIMObjectProperties
|
||||
return obj.BIMObjectProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_object_attribute_props(cls, obj: bpy.types.Object) -> BIMAttributeProperties:
|
||||
return obj.BIMAttributeProperties
|
||||
return obj.BIMAttributeProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_solar_props(cls) -> BIMSolarProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMSolarProperties
|
||||
return scene.BIMSolarProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_radiance_exporter_props(cls) -> RadianceExporterProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMRadianceExporeterProperies
|
||||
return scene.BIMRadianceExporeterProperies # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_fm_props(cls) -> BIMFMProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMFMProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_ifc_definition_id(cls, obj: IFC_CONNECTED_TYPE) -> int:
|
||||
|
||||
@@ -45,10 +45,11 @@ from mathutils import Color
|
||||
if TYPE_CHECKING:
|
||||
import bonsai.bim.prop
|
||||
from bonsai.bim.module.sequence.prop import (
|
||||
BIMTaskTreeProperties,
|
||||
BIMWorkScheduleProperties,
|
||||
BIMAnimationProperties,
|
||||
BIMStatusProperties,
|
||||
BIMTaskTreeProperties,
|
||||
BIMWorkPlanProperties,
|
||||
BIMWorkScheduleProperties,
|
||||
)
|
||||
|
||||
|
||||
@@ -58,21 +59,29 @@ class Sequence(bonsai.core.tool.Sequence):
|
||||
|
||||
@classmethod
|
||||
def get_animation_props(cls) -> BIMAnimationProperties:
|
||||
return bpy.context.scene.BIMAnimationProperties
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMAnimationProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_task_tree_props(cls) -> BIMTaskTreeProperties:
|
||||
return bpy.context.scene.BIMTaskTreeProperties
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMTaskTreeProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_work_schedule_props(cls) -> BIMWorkScheduleProperties:
|
||||
return bpy.context.scene.BIMWorkScheduleProperties
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMWorkScheduleProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_status_props(cls) -> BIMStatusProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMStatusProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_work_plan_props(cls) -> BIMWorkPlanProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMWorkPlanProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_work_plan_attributes(cls) -> dict[str, Any]:
|
||||
import bonsai.bim.module.sequence.helper as helper
|
||||
@@ -91,7 +100,7 @@ class Sequence(bonsai.core.tool.Sequence):
|
||||
attributes[prop.name] = helper.parse_duration(prop.string_value)
|
||||
return True
|
||||
|
||||
props = bpy.context.scene.BIMWorkPlanProperties
|
||||
props = cls.get_work_plan_props()
|
||||
return bonsai.bim.helper.export_attributes(props.work_plan_attributes, callback)
|
||||
|
||||
@classmethod
|
||||
@@ -101,25 +110,28 @@ class Sequence(bonsai.core.tool.Sequence):
|
||||
prop.string_value = "" if prop.is_null else data[name]
|
||||
return True
|
||||
|
||||
props = bpy.context.scene.BIMWorkPlanProperties
|
||||
props = cls.get_work_plan_props()
|
||||
props.work_plan_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes2(work_plan, props.work_plan_attributes, callback)
|
||||
|
||||
@classmethod
|
||||
def enable_editing_work_plan(cls, work_plan: Union[ifcopenshell.entity_instance, None]) -> None:
|
||||
if work_plan:
|
||||
bpy.context.scene.BIMWorkPlanProperties.active_work_plan_id = work_plan.id()
|
||||
bpy.context.scene.BIMWorkPlanProperties.editing_type = "ATTRIBUTES"
|
||||
props = cls.get_work_plan_props()
|
||||
props.active_work_plan_id = work_plan.id()
|
||||
props.editing_type = "ATTRIBUTES"
|
||||
|
||||
@classmethod
|
||||
def disable_editing_work_plan(cls) -> None:
|
||||
bpy.context.scene.BIMWorkPlanProperties.active_work_plan_id = 0
|
||||
props = cls.get_work_plan_props()
|
||||
props.active_work_plan_id = 0
|
||||
|
||||
@classmethod
|
||||
def enable_editing_work_plan_schedules(cls, work_plan: Union[ifcopenshell.entity_instance, None]) -> None:
|
||||
if work_plan:
|
||||
bpy.context.scene.BIMWorkPlanProperties.active_work_plan_id = work_plan.id()
|
||||
bpy.context.scene.BIMWorkPlanProperties.editing_type = "SCHEDULES"
|
||||
props = cls.get_work_plan_props()
|
||||
props.active_work_plan_id = work_plan.id()
|
||||
props.editing_type = "SCHEDULES"
|
||||
|
||||
@classmethod
|
||||
def get_work_schedule_attributes(cls) -> dict[str, Any]:
|
||||
|
||||
@@ -318,12 +318,21 @@ class Iterator:
|
||||
def get_native(self): ...
|
||||
def get_object(self, id): ...
|
||||
def had_error_processing_elements(self): ...
|
||||
def initialize(self): ...
|
||||
def initialize(self) -> bool:
|
||||
"""Return true if the iterator is initialized with any elements, false otherwise."""
|
||||
...
|
||||
|
||||
def next(self): ...
|
||||
def process_concurrently(self): ...
|
||||
def process_finished_rep(self, rep): ...
|
||||
def progress(self): ...
|
||||
def set_cache(self, cache): ...
|
||||
def progress(self) -> int:
|
||||
"""Return current progress (0-100).
|
||||
|
||||
Updated after processing each representation.
|
||||
"""
|
||||
...
|
||||
|
||||
def set_cache(self, cache: GeometrySerializer): ...
|
||||
def unit_magnitude(self): ...
|
||||
def unit_name(self): ...
|
||||
|
||||
|
||||
Reference in New Issue
Block a user