Draft: clean up OrthoArray code
Avoid `Draft.py` in the `make_ortho_array` function because it creates a circular dependency. Use internal functions to abstract the orthogonal arrays. The `_make_ortho_array` is a simple wrapper over the general `make_array` function. The result is four variants: - `make_ortho_array` - `make_ortho_array2d`, without the Z component - `make_rect_array`, strictly rectangular components - `make_rect_array2d`, strictly rectangular without the Z component Also use functions to perform checking of the inputs, whether these are vectors, numbers (integer and floats) or integers. Now the make function accepts as input a `"String"` which must be the `Label` of an object in the document, so it is easier to create arrays quickly from the Python console. Clean up the GuiCommand and task panel code, and avoid printing messages to the terminal, as this is already done by the make function.
This commit is contained in:
@@ -26,14 +26,155 @@
|
||||
# \brief Provides functions for creating orthogonal arrays in 2D and 3D.
|
||||
|
||||
import FreeCAD as App
|
||||
import Draft
|
||||
# import draftmake.make_array as make_array
|
||||
|
||||
import draftmake.make_array as make_array
|
||||
import draftutils.utils as utils
|
||||
|
||||
from draftutils.messages import _msg, _wrn, _err
|
||||
from draftutils.translate import _tr
|
||||
|
||||
|
||||
def make_ortho_array(obj,
|
||||
def _make_ortho_array(base_object,
|
||||
v_x=App.Vector(10, 0, 0),
|
||||
v_y=App.Vector(0, 10, 0),
|
||||
v_z=App.Vector(0, 0, 10),
|
||||
n_x=2,
|
||||
n_y=2,
|
||||
n_z=1,
|
||||
use_link=True):
|
||||
"""Create an orthogonal array from the given object.
|
||||
|
||||
This is a simple wrapper of the `draftmake.make_array.make_array`
|
||||
function to be used by the different orthogonal arrays.
|
||||
|
||||
- `make_ortho_array`
|
||||
- `make_ortho_array2d`, no Z direction
|
||||
- `make_rect_array`, strictly rectangular
|
||||
- `make_rect_array2d`, strictly rectangular, no Z direction
|
||||
|
||||
This function has no error checking, nor does it display messages.
|
||||
This should be handled by the subfunctions that use this one.
|
||||
"""
|
||||
_name = "_make_ortho_array"
|
||||
utils.print_header(_name, _tr("Internal orthogonal array"), debug=False)
|
||||
|
||||
new_obj = make_array.make_array(base_object,
|
||||
arg1=v_x, arg2=v_y, arg3=v_z,
|
||||
arg4=n_x, arg5=n_y, arg6=n_z,
|
||||
use_link=use_link)
|
||||
return new_obj
|
||||
|
||||
|
||||
def _are_vectors(v_x, v_y, v_z=None, name="Unknown"):
|
||||
"""Check that the vectors are numbers."""
|
||||
_msg("v_x: {}".format(v_x))
|
||||
_msg("v_y: {}".format(v_y))
|
||||
if v_z:
|
||||
_msg("v_z: {}".format(v_z))
|
||||
|
||||
try:
|
||||
if v_z:
|
||||
utils.type_check([(v_x, (int, float, App.Vector)),
|
||||
(v_y, (int, float, App.Vector)),
|
||||
(v_z, (int, float, App.Vector))],
|
||||
name=name)
|
||||
else:
|
||||
utils.type_check([(v_x, (int, float, App.Vector)),
|
||||
(v_y, (int, float, App.Vector))],
|
||||
name=name)
|
||||
except TypeError:
|
||||
_err(_tr("Wrong input: must be a number or vector."))
|
||||
return False, v_x, v_y, v_z
|
||||
|
||||
_text = "Input: single value expanded to vector."
|
||||
if not isinstance(v_x, App.Vector):
|
||||
v_x = App.Vector(v_x, 0, 0)
|
||||
_wrn(_tr(_text))
|
||||
if not isinstance(v_y, App.Vector):
|
||||
v_y = App.Vector(0, v_y, 0)
|
||||
_wrn(_tr(_text))
|
||||
if v_z and not isinstance(v_z, App.Vector):
|
||||
v_z = App.Vector(0, 0, v_z)
|
||||
_wrn(_tr(_text))
|
||||
|
||||
return True, v_x, v_y, v_z
|
||||
|
||||
|
||||
def _are_integers(n_x, n_y, n_z=None, name="Unknown"):
|
||||
"""Check that the numbers are integers, with minimum value of 1."""
|
||||
_msg("n_x: {}".format(n_x))
|
||||
_msg("n_y: {}".format(n_y))
|
||||
if n_z:
|
||||
_msg("n_z: {}".format(n_z))
|
||||
|
||||
try:
|
||||
if n_z:
|
||||
utils.type_check([(n_x, int),
|
||||
(n_y, int),
|
||||
(n_z, int)], name=name)
|
||||
else:
|
||||
utils.type_check([(n_x, int),
|
||||
(n_y, int)], name=name)
|
||||
except TypeError:
|
||||
_err(_tr("Wrong input: must be an integer number."))
|
||||
return False, n_x, n_y, n_z
|
||||
|
||||
_text = ("Input: number of elements must be at least 1. "
|
||||
"It is set to 1.")
|
||||
if n_x < 1:
|
||||
_wrn(_tr(_text))
|
||||
n_x = 1
|
||||
if n_y < 1:
|
||||
_wrn(_tr(_text))
|
||||
n_y = 1
|
||||
if n_z and n_z < 1:
|
||||
_wrn(_tr(_text))
|
||||
n_z = 1
|
||||
|
||||
return True, n_x, n_y, n_z
|
||||
|
||||
|
||||
def _are_numbers(d_x, d_y, d_z=None, name="Unknown"):
|
||||
"""Check that the numbers are numbers."""
|
||||
_msg("d_x: {}".format(d_x))
|
||||
_msg("d_y: {}".format(d_y))
|
||||
if d_z:
|
||||
_msg("d_z: {}".format(d_z))
|
||||
|
||||
try:
|
||||
if d_z:
|
||||
utils.type_check([(d_x, (int, float)),
|
||||
(d_y, (int, float)),
|
||||
(d_z, (int, float))], name=name)
|
||||
else:
|
||||
utils.type_check([(d_x, (int, float)),
|
||||
(d_y, (int, float))], name=name)
|
||||
except TypeError:
|
||||
_err(_tr("Wrong input: must be a number."))
|
||||
return False, d_x, d_y, d_z
|
||||
|
||||
return True, d_x, d_y, d_z
|
||||
|
||||
|
||||
def _find_object_in_doc(base_object, doc=None):
|
||||
"""Check that a document is available and the object exists."""
|
||||
FOUND = True
|
||||
if isinstance(base_object, str):
|
||||
base_object_str = base_object
|
||||
|
||||
found, base_object = utils.find_object(base_object,
|
||||
doc=doc)
|
||||
if not found:
|
||||
_msg("base_object: {}".format(base_object_str))
|
||||
_err(_tr("Wrong input: object not in document."))
|
||||
return not FOUND, base_object
|
||||
|
||||
_msg("base_object: {}".format(base_object.Label))
|
||||
|
||||
return FOUND, base_object
|
||||
|
||||
|
||||
def make_ortho_array(base_object,
|
||||
v_x=App.Vector(10, 0, 0),
|
||||
v_y=App.Vector(0, 10, 0),
|
||||
v_z=App.Vector(0, 0, 10),
|
||||
@@ -45,11 +186,12 @@ def make_ortho_array(obj,
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj: Part::Feature
|
||||
Any type of object that has a `Part::TopoShape`
|
||||
that can be duplicated.
|
||||
This means most 2D and 3D objects produced
|
||||
with any workbench.
|
||||
base_object: Part::Feature or str
|
||||
Any of object that has a `Part::TopoShape` that can be duplicated.
|
||||
This means most 2D and 3D objects produced with any workbench.
|
||||
If it is a string, it must be the `Label` of that object.
|
||||
Since a label is not guaranteed to be unique in a document,
|
||||
it will use the first object found with this label.
|
||||
|
||||
v_x, v_y, v_z: Base::Vector3, optional
|
||||
The vector indicating the vector displacement between two elements
|
||||
@@ -99,13 +241,13 @@ def make_ortho_array(obj,
|
||||
|
||||
The values of `n_x` and `n_y` default to 2,
|
||||
while `n_z` defaults to 1.
|
||||
This means the array by default is a planar array.
|
||||
This means the array is a planar array by default.
|
||||
|
||||
use_link: bool, optional
|
||||
It defaults to `True`.
|
||||
If it is `True` the produced copies are not `Part::TopoShape` copies,
|
||||
but rather `App::Link` objects.
|
||||
The Links repeat the shape of the original `obj` exactly,
|
||||
The Links repeat the shape of the original `base_object` exactly,
|
||||
and therefore the resulting array is more memory efficient.
|
||||
|
||||
Also, when `use_link` is `True`, the `Fuse` property
|
||||
@@ -120,75 +262,44 @@ def make_ortho_array(obj,
|
||||
Returns
|
||||
-------
|
||||
Part::FeaturePython
|
||||
A scripted object with `Proxy.Type='Array'`.
|
||||
A scripted object of type `'Array'`.
|
||||
Its `Shape` is a compound of the copies of the original object.
|
||||
|
||||
None
|
||||
If there is a problem it will return `None`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
make_ortho_array2d, make_rect_array, make_rect_array2d
|
||||
make_ortho_array2d, make_rect_array, make_rect_array2d, make_polar_array,
|
||||
make_circular_array, make_path_array, make_point_array
|
||||
"""
|
||||
_name = "make_ortho_array"
|
||||
utils.print_header(_name, _tr("Orthogonal array"))
|
||||
|
||||
_msg("v_x: {}".format(v_x))
|
||||
_msg("v_y: {}".format(v_y))
|
||||
_msg("v_z: {}".format(v_z))
|
||||
|
||||
try:
|
||||
utils.type_check([(v_x, (int, float, App.Vector)),
|
||||
(v_y, (int, float, App.Vector)),
|
||||
(v_z, (int, float, App.Vector))],
|
||||
name=_name)
|
||||
except TypeError:
|
||||
_err(_tr("Wrong input: must be a number or vector."))
|
||||
found, base_object = _find_object_in_doc(base_object,
|
||||
doc=App.activeDocument())
|
||||
if not found:
|
||||
return None
|
||||
|
||||
_text = "Input: single value expanded to vector."
|
||||
if not isinstance(v_x, App.Vector):
|
||||
v_x = App.Vector(v_x, 0, 0)
|
||||
_wrn(_tr(_text))
|
||||
if not isinstance(v_y, App.Vector):
|
||||
v_y = App.Vector(0, v_y, 0)
|
||||
_wrn(_tr(_text))
|
||||
if not isinstance(v_z, App.Vector):
|
||||
v_z = App.Vector(0, 0, v_z)
|
||||
_wrn(_tr(_text))
|
||||
|
||||
_msg("n_x: {}".format(n_x))
|
||||
_msg("n_y: {}".format(n_y))
|
||||
_msg("n_z: {}".format(n_z))
|
||||
|
||||
try:
|
||||
utils.type_check([(n_x, int),
|
||||
(n_y, int),
|
||||
(n_z, int)], name=_name)
|
||||
except TypeError:
|
||||
_err(_tr("Wrong input: must be an integer number."))
|
||||
ok, v_x, v_y, v_z = _are_vectors(v_x, v_y, v_z, name=_name)
|
||||
if not ok:
|
||||
return None
|
||||
|
||||
_text = ("Input: number of elements must be at least 1. "
|
||||
"It is set to 1.")
|
||||
if n_x < 1:
|
||||
_wrn(_tr(_text))
|
||||
n_x = 1
|
||||
if n_y < 1:
|
||||
_wrn(_tr(_text))
|
||||
n_y = 1
|
||||
if n_z < 1:
|
||||
_wrn(_tr(_text))
|
||||
n_z = 1
|
||||
ok, n_x, n_y, n_z = _are_integers(n_x, n_y, n_z, name=_name)
|
||||
if not ok:
|
||||
return None
|
||||
|
||||
_msg("use_link: {}".format(bool(use_link)))
|
||||
use_link = bool(use_link)
|
||||
_msg("use_link: {}".format(use_link))
|
||||
|
||||
# new_obj = make_array.make_array()
|
||||
new_obj = Draft.makeArray(obj,
|
||||
arg1=v_x, arg2=v_y, arg3=v_z,
|
||||
arg4=n_x, arg5=n_y, arg6=n_z,
|
||||
use_link=use_link)
|
||||
new_obj = _make_ortho_array(base_object,
|
||||
v_x=v_x, v_y=v_y, v_z=v_z,
|
||||
n_x=n_x, n_y=n_y, n_z=n_z,
|
||||
use_link=use_link)
|
||||
return new_obj
|
||||
|
||||
|
||||
def make_ortho_array2d(obj,
|
||||
def make_ortho_array2d(base_object,
|
||||
v_x=App.Vector(10, 0, 0),
|
||||
v_y=App.Vector(0, 10, 0),
|
||||
n_x=2,
|
||||
@@ -202,11 +313,12 @@ def make_ortho_array2d(obj,
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj: Part::Feature
|
||||
Any type of object that has a `Part::TopoShape`
|
||||
that can be duplicated.
|
||||
This means most 2D and 3D objects produced
|
||||
with any workbench.
|
||||
base_object: Part::Feature or str
|
||||
Any of object that has a `Part::TopoShape` that can be duplicated.
|
||||
This means most 2D and 3D objects produced with any workbench.
|
||||
If it is a string, it must be the `Label` of that object.
|
||||
Since a label is not guaranteed to be unique in a document,
|
||||
it will use the first object found with this label.
|
||||
|
||||
v_x, v_y: Base::Vector3, optional
|
||||
Vectorial displacement of elements
|
||||
@@ -225,65 +337,44 @@ def make_ortho_array2d(obj,
|
||||
Returns
|
||||
-------
|
||||
Part::FeaturePython
|
||||
A scripted object with `Proxy.Type='Array'`.
|
||||
A scripted object of type `'Array'`.
|
||||
Its `Shape` is a compound of the copies of the original object.
|
||||
|
||||
None
|
||||
If there is a problem it will return `None`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
make_ortho_array, make_rect_array, make_rect_array2d
|
||||
make_ortho_array, make_rect_array, make_rect_array2d, make_polar_array,
|
||||
make_circular_array, make_path_array, make_point_array
|
||||
"""
|
||||
_name = "make_ortho_array2d"
|
||||
utils.print_header(_name, _tr("Orthogonal array 2D"))
|
||||
|
||||
_msg("v_x: {}".format(v_x))
|
||||
_msg("v_y: {}".format(v_y))
|
||||
|
||||
try:
|
||||
utils.type_check([(v_x, (int, float, App.Vector)),
|
||||
(v_y, (int, float, App.Vector))],
|
||||
name=_name)
|
||||
except TypeError:
|
||||
_err(_tr("Wrong input: must be a number or vector."))
|
||||
found, base_object = _find_object_in_doc(base_object,
|
||||
doc=App.activeDocument())
|
||||
if not found:
|
||||
return None
|
||||
|
||||
_text = "Input: single value expanded to vector."
|
||||
if not isinstance(v_x, App.Vector):
|
||||
v_x = App.Vector(v_x, 0, 0)
|
||||
_wrn(_tr(_text))
|
||||
if not isinstance(v_y, App.Vector):
|
||||
v_y = App.Vector(0, v_y, 0)
|
||||
_wrn(_tr(_text))
|
||||
|
||||
_msg("n_x: {}".format(n_x))
|
||||
_msg("n_y: {}".format(n_y))
|
||||
|
||||
try:
|
||||
utils.type_check([(n_x, int),
|
||||
(n_y, int)], name=_name)
|
||||
except TypeError:
|
||||
_err(_tr("Wrong input: must be an integer number."))
|
||||
ok, v_x, v_y, __ = _are_vectors(v_x, v_y, v_z=None, name=_name)
|
||||
if not ok:
|
||||
return None
|
||||
|
||||
_text = ("Input: number of elements must be at least 1. "
|
||||
"It is set to 1.")
|
||||
if n_x < 1:
|
||||
_wrn(_tr(_text))
|
||||
n_x = 1
|
||||
if n_y < 1:
|
||||
_wrn(_tr(_text))
|
||||
n_y = 1
|
||||
ok, n_x, n_y, __ = _are_integers(n_x, n_y, n_z=None, name=_name)
|
||||
if not ok:
|
||||
return None
|
||||
|
||||
_msg("use_link: {}".format(bool(use_link)))
|
||||
use_link = bool(use_link)
|
||||
_msg("use_link: {}".format(use_link))
|
||||
|
||||
# new_obj = make_array.make_array()
|
||||
new_obj = Draft.makeArray(obj,
|
||||
arg1=v_x, arg2=v_y,
|
||||
arg3=n_x, arg4=n_y,
|
||||
use_link=use_link)
|
||||
new_obj = _make_ortho_array(base_object,
|
||||
v_x=v_x, v_y=v_y,
|
||||
n_x=n_x, n_y=n_y,
|
||||
use_link=use_link)
|
||||
return new_obj
|
||||
|
||||
|
||||
def make_rect_array(obj,
|
||||
def make_rect_array(base_object,
|
||||
d_x=10,
|
||||
d_y=10,
|
||||
d_z=10,
|
||||
@@ -300,11 +391,12 @@ def make_rect_array(obj,
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj: Part::Feature
|
||||
Any type of object that has a `Part::TopoShape`
|
||||
that can be duplicated.
|
||||
This means most 2D and 3D objects produced
|
||||
with any workbench.
|
||||
base_object: Part::Feature or str
|
||||
Any of object that has a `Part::TopoShape` that can be duplicated.
|
||||
This means most 2D and 3D objects produced with any workbench.
|
||||
If it is a string, it must be the `Label` of that object.
|
||||
Since a label is not guaranteed to be unique in a document,
|
||||
it will use the first object found with this label.
|
||||
|
||||
d_x, d_y, d_z: Base::Vector3, optional
|
||||
Displacement of elements in the corresponding X, Y, and Z directions.
|
||||
@@ -319,41 +411,48 @@ def make_rect_array(obj,
|
||||
Returns
|
||||
-------
|
||||
Part::FeaturePython
|
||||
A scripted object with `Proxy.Type='Array'`.
|
||||
A scripted object of type `'Array'`.
|
||||
Its `Shape` is a compound of the copies of the original object.
|
||||
|
||||
None
|
||||
If there is a problem it will return `None`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
make_ortho_array, make_ortho_array2d, make_rect_array2d
|
||||
make_ortho_array, make_ortho_array2d, make_rect_array2d, make_polar_array,
|
||||
make_circular_array, make_path_array, make_point_array
|
||||
"""
|
||||
_name = "make_rect_array"
|
||||
utils.print_header(_name, _tr("Rectangular array"))
|
||||
|
||||
_msg("d_x: {}".format(d_x))
|
||||
_msg("d_y: {}".format(d_y))
|
||||
_msg("d_z: {}".format(d_z))
|
||||
|
||||
try:
|
||||
utils.type_check([(d_x, (int, float)),
|
||||
(d_y, (int, float)),
|
||||
(d_z, (int, float))],
|
||||
name=_name)
|
||||
except TypeError:
|
||||
_err(_tr("Wrong input: must be a number."))
|
||||
found, base_object = _find_object_in_doc(base_object,
|
||||
doc=App.activeDocument())
|
||||
if not found:
|
||||
return None
|
||||
|
||||
new_obj = make_ortho_array(obj,
|
||||
v_x=App.Vector(d_x, 0, 0),
|
||||
v_y=App.Vector(0, d_y, 0),
|
||||
v_z=App.Vector(0, 0, d_z),
|
||||
n_x=n_x,
|
||||
n_y=n_y,
|
||||
n_z=n_z,
|
||||
use_link=use_link)
|
||||
ok, d_x, d_y, d_z = _are_numbers(d_x, d_y, d_z, name=_name)
|
||||
if not ok:
|
||||
return None
|
||||
|
||||
ok, n_x, n_y, n_z = _are_integers(n_x, n_y, n_z, _name)
|
||||
if not ok:
|
||||
return None
|
||||
|
||||
use_link = bool(use_link)
|
||||
_msg("use_link: {}".format(use_link))
|
||||
|
||||
new_obj = _make_ortho_array(base_object,
|
||||
v_x=App.Vector(d_x, 0, 0),
|
||||
v_y=App.Vector(0, d_y, 0),
|
||||
v_z=App.Vector(0, 0, d_z),
|
||||
n_x=n_x,
|
||||
n_y=n_y,
|
||||
n_z=n_z,
|
||||
use_link=use_link)
|
||||
return new_obj
|
||||
|
||||
|
||||
def make_rect_array2d(obj,
|
||||
def make_rect_array2d(base_object,
|
||||
d_x=10,
|
||||
d_y=10,
|
||||
n_x=2,
|
||||
@@ -361,18 +460,20 @@ def make_rect_array2d(obj,
|
||||
use_link=True):
|
||||
"""Create a 2D rectangular array from the given object.
|
||||
|
||||
This function wraps around `make_ortho_array2d`
|
||||
This function wraps around `make_ortho_array`,
|
||||
to produce strictly rectangular arrays, in which
|
||||
the displacement vectors `v_x` and `v_y`
|
||||
only have their respective components in X and Y.
|
||||
The Z component is ignored.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj: Part::Feature
|
||||
Any type of object that has a `Part::TopoShape`
|
||||
that can be duplicated.
|
||||
This means most 2D and 3D objects produced
|
||||
with any workbench.
|
||||
base_object: Part::Feature or str
|
||||
Any of object that has a `Part::TopoShape` that can be duplicated.
|
||||
This means most 2D and 3D objects produced with any workbench.
|
||||
If it is a string, it must be the `Label` of that object.
|
||||
Since a label is not guaranteed to be unique in a document,
|
||||
it will use the first object found with this label.
|
||||
|
||||
d_x, d_y: Base::Vector3, optional
|
||||
Displacement of elements in the corresponding X and Y directions.
|
||||
@@ -387,31 +488,40 @@ def make_rect_array2d(obj,
|
||||
Returns
|
||||
-------
|
||||
Part::FeaturePython
|
||||
A scripted object with `Proxy.Type='Array'`.
|
||||
A scripted object of type `'Array'`.
|
||||
Its `Shape` is a compound of the copies of the original object.
|
||||
|
||||
None
|
||||
If there is a problem it will return `None`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
make_ortho_array, make_ortho_array2d, make_rect_array
|
||||
make_ortho_array, make_ortho_array2d, make_rect_array, make_polar_array,
|
||||
make_circular_array, make_path_array, make_point_array
|
||||
"""
|
||||
_name = "make_rect_array2d"
|
||||
utils.print_header(_name, _tr("Rectangular array 2D"))
|
||||
|
||||
_msg("d_x: {}".format(d_x))
|
||||
_msg("d_y: {}".format(d_y))
|
||||
|
||||
try:
|
||||
utils.type_check([(d_x, (int, float)),
|
||||
(d_y, (int, float))],
|
||||
name=_name)
|
||||
except TypeError:
|
||||
_err(_tr("Wrong input: must be a number."))
|
||||
found, base_object = _find_object_in_doc(base_object,
|
||||
doc=App.activeDocument())
|
||||
if not found:
|
||||
return None
|
||||
|
||||
new_obj = make_ortho_array2d(obj,
|
||||
v_x=App.Vector(d_x, 0, 0),
|
||||
v_y=App.Vector(0, d_y, 0),
|
||||
n_x=n_x,
|
||||
n_y=n_y,
|
||||
use_link=use_link)
|
||||
ok, d_x, d_y, __ = _are_numbers(d_x, d_y, d_z=None, name=_name)
|
||||
if not ok:
|
||||
return None
|
||||
|
||||
ok, n_x, n_y, __ = _are_integers(n_x, n_y, n_z=None, name=_name)
|
||||
if not ok:
|
||||
return None
|
||||
|
||||
use_link = bool(use_link)
|
||||
_msg("use_link: {}".format(use_link))
|
||||
|
||||
new_obj = _make_ortho_array(base_object,
|
||||
v_x=App.Vector(d_x, 0, 0),
|
||||
v_y=App.Vector(0, d_y, 0),
|
||||
n_x=n_x,
|
||||
n_y=n_y,
|
||||
use_link=use_link)
|
||||
return new_obj
|
||||
|
||||
Reference in New Issue
Block a user