initial working version of cqgi
This commit is contained in:
parent
c12e663638
commit
4e2168cad6
206
cadquery/cqgi.py
206
cadquery/cqgi.py
|
@ -9,16 +9,17 @@ import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
CQSCRIPT = "<cqscript>"
|
CQSCRIPT = "<cqscript>"
|
||||||
BUILD_OBJECT_COLLECTOR = "__boc__"
|
|
||||||
|
|
||||||
class CQModel(object):
|
class CQModel(object):
|
||||||
"""
|
"""
|
||||||
Object that provides a nice interface to a cq script that
|
Object that provides a nice interface to a cq script that
|
||||||
is following the cce model
|
is following the cce model
|
||||||
"""
|
"""
|
||||||
def __init__(self,scriptSource):
|
|
||||||
|
def __init__(self, script_source):
|
||||||
self.metadata = ScriptMetadata()
|
self.metadata = ScriptMetadata()
|
||||||
self.astTree = ast.parse(scriptSource,CQSCRIPT)
|
self.astTree = ast.parse(script_source, CQSCRIPT)
|
||||||
|
|
||||||
ConstantAssignmentFinder(self.metadata).visit(self.astTree)
|
ConstantAssignmentFinder(self.metadata).visit(self.astTree)
|
||||||
|
|
||||||
|
@ -29,62 +30,64 @@ class CQModel(object):
|
||||||
def validate(self, params):
|
def validate(self, params):
|
||||||
raise NotImplementedError("not yet implemented")
|
raise NotImplementedError("not yet implemented")
|
||||||
|
|
||||||
def build(self,params):
|
def build(self, params=None):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
:param params: dictionary of parameter values to build with
|
:param params: dictionary of parameter values to build with
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
self.setParamValues(params)
|
if not params:
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
self.set_param_values(params)
|
||||||
collector = BuildObjectCollector()
|
collector = BuildObjectCollector()
|
||||||
env = EnvironmentBuilder().withRealBuiltins() \
|
env = EnvironmentBuilder().with_real_builtins() \
|
||||||
.addEntry("build_object",collector.build_object).build()
|
.add_entry("build_object", collector.build_object).build()
|
||||||
|
|
||||||
start = time.clock()
|
start = time.clock()
|
||||||
result = BuildResult()
|
result = BuildResult()
|
||||||
|
|
||||||
|
try:
|
||||||
c = compile(self.astTree, CQSCRIPT, 'exec')
|
c = compile(self.astTree, CQSCRIPT, 'exec')
|
||||||
exec (c, env)
|
exec (c, env)
|
||||||
if collector.hasResults():
|
if collector.hasResults():
|
||||||
result.setSuccessResult(collector.outputObjects)
|
result.set_success_result(collector.outputObjects)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Script did not call build_object-- no output available.")
|
raise ValueError("Script did not call build_object-- no output available.")
|
||||||
#except Exception,ex:
|
except Exception, ex:
|
||||||
|
result.set_failure_result(ex)
|
||||||
# result.setFailureResult(ex)
|
|
||||||
|
|
||||||
end = time.clock()
|
end = time.clock()
|
||||||
result.buildTime = end - start
|
result.buildTime = end - start
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def setParamValues(self,params):
|
def set_param_values(self, params):
|
||||||
modelParameters = self.metadata.parameters
|
model_parameters = self.metadata.parameters
|
||||||
|
|
||||||
for k, v in params.iteritems():
|
for k, v in params.iteritems():
|
||||||
if not modelParameters.has_key(k):
|
if k not in model_parameters:
|
||||||
raise ValueError("Cannot set value '%s', it is not a parameter of the model." % k)
|
raise InvalidParameterError("Cannot set value '%s': not a parameter of the model." % k)
|
||||||
|
|
||||||
p = modelParameters[k]
|
p = model_parameters[k]
|
||||||
p.setValue(v)
|
p.set_value(v)
|
||||||
|
|
||||||
|
|
||||||
class BuildResult(object):
|
class BuildResult(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.buildTime = None
|
self.buildTime = None
|
||||||
self.results = []
|
self.results = []
|
||||||
self.success = False
|
self.success = False
|
||||||
self.exception = None
|
self.exception = None
|
||||||
|
|
||||||
def setFailureResult(self,ex):
|
def set_failure_result(self, ex):
|
||||||
self.exception = ex
|
self.exception = ex
|
||||||
self.success = False
|
self.success = False
|
||||||
|
|
||||||
def setSuccessResult(self,results):
|
def set_success_result(self, results):
|
||||||
self.results = results
|
self.results = results
|
||||||
self.success = True
|
self.success = True
|
||||||
|
|
||||||
|
|
||||||
class ScriptMetadata(object):
|
class ScriptMetadata(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.parameters = {}
|
self.parameters = {}
|
||||||
|
@ -92,57 +95,86 @@ class ScriptMetadata(object):
|
||||||
def add_script_parameter(self, p):
|
def add_script_parameter(self, p):
|
||||||
self.parameters[p.name] = p
|
self.parameters[p.name] = p
|
||||||
|
|
||||||
class ParameterType(object): pass
|
|
||||||
class NumberParameterType(ParameterType) : pass
|
class ParameterType(object):
|
||||||
class StringParameterType(ParameterType) : pass
|
pass
|
||||||
class BooleanParameterType(ParameterType): pass
|
|
||||||
|
|
||||||
|
class NumberParameterType(ParameterType):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class StringParameterType(ParameterType):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BooleanParameterType(ParameterType):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InputParameter:
|
class InputParameter:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.name = None
|
self.name = None
|
||||||
self.shortDesc = None
|
self.shortDesc = None
|
||||||
self.varType = None
|
self.varType = None
|
||||||
self.validValues = []
|
self.validValues = []
|
||||||
self.defaultValue= None
|
self.default_value = None
|
||||||
self.astNode = None
|
self.ast_node = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create(astNode,varname,varType, defaultValue,validValues =[],shortDesc = None):
|
def create(ast_node, var_name, var_type, default_value, valid_values=None, short_desc=None):
|
||||||
|
|
||||||
|
if valid_values is None:
|
||||||
|
valid_values = []
|
||||||
|
|
||||||
p = InputParameter()
|
p = InputParameter()
|
||||||
p.astNode = astNode
|
p.ast_node = ast_node
|
||||||
p.defaultValue = defaultValue
|
p.default_value = default_value
|
||||||
p.name = varname
|
p.name = var_name
|
||||||
if shortDesc == None:
|
if short_desc is None:
|
||||||
p.shortDesc = varname
|
p.shortDesc = var_name
|
||||||
else:
|
else:
|
||||||
p.shortDesc = shortDesc
|
p.shortDesc = short_desc
|
||||||
p.varType = varType
|
p.varType = var_type
|
||||||
p.validValues = validValues
|
p.validValues = valid_values
|
||||||
return p
|
return p
|
||||||
|
|
||||||
def setValue(self,newValue):
|
def set_value(self, new_value):
|
||||||
#todo: check to make sure newValue can be cast correctly?
|
|
||||||
|
if len(self.validValues) > 0 and not new_value in self.validValues:
|
||||||
|
raise InvalidParameterError(
|
||||||
|
"Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} "
|
||||||
|
.format( str(new_value), self.name, str(self.validValues)))
|
||||||
|
|
||||||
if self.varType == NumberParameterType:
|
if self.varType == NumberParameterType:
|
||||||
self.astNode.n = newValue
|
try:
|
||||||
|
f = float(new_value)
|
||||||
|
self.ast_node.n = f
|
||||||
|
except ValueError:
|
||||||
|
raise InvalidParameterError(
|
||||||
|
"Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric."
|
||||||
|
.format(str(new_value), self.name))
|
||||||
|
|
||||||
elif self.varType == StringParameterType:
|
elif self.varType == StringParameterType:
|
||||||
self.astNode.s = str(newValue)
|
self.ast_node.s = str(new_value)
|
||||||
elif self.varType == BooleanParameterType:
|
elif self.varType == BooleanParameterType:
|
||||||
if ( newValue ):
|
if new_value:
|
||||||
self.astNode.value.id = 'True'
|
self.ast_node.value.id = 'True'
|
||||||
else:
|
else:
|
||||||
self.astNode.value.id = 'False'
|
self.ast_node.value.id = 'False'
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown Type of var: ", str(self.varType))
|
raise ValueError("Unknown Type of var: ", str(self.varType))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "InputParmaeter: {name=%s, type=%s, defaultValue=%s" % ( self.name, str(self.varType), str(self.defaultValue) )
|
return "InputParameter: {name=%s, type=%s, defaultValue=%s" % (
|
||||||
|
self.name, str(self.varType), str(self.default_value))
|
||||||
|
|
||||||
|
|
||||||
class BuildObjectCollector(object):
|
class BuildObjectCollector(object):
|
||||||
"""
|
"""
|
||||||
Allows a script to provide output objects
|
Allows a script to provide output objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.outputObjects = []
|
self.outputObjects = []
|
||||||
|
|
||||||
|
@ -152,10 +184,12 @@ class BuildObjectCollector(object):
|
||||||
def hasResults(self):
|
def hasResults(self):
|
||||||
return len(self.outputObjects) > 0
|
return len(self.outputObjects) > 0
|
||||||
|
|
||||||
|
|
||||||
class ScriptExecutor(object):
|
class ScriptExecutor(object):
|
||||||
"""
|
"""
|
||||||
executes a script in a given environment.
|
executes a script in a given environment.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, environment, astTree):
|
def __init__(self, environment, astTree):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -166,26 +200,32 @@ class ScriptExecutor(object):
|
||||||
# try to figure out what line the error was on
|
# try to figure out what line the error was on
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
formatted_lines = traceback.format_exc().splitlines()
|
formatted_lines = traceback.format_exc().splitlines()
|
||||||
lineText = ""
|
line_text = ""
|
||||||
for f in formatted_lines:
|
for f in formatted_lines:
|
||||||
if f.find(CQSCRIPT) > -1:
|
if f.find(CQSCRIPT) > -1:
|
||||||
m = re.search("line\\s+(\\d+)", f, re.IGNORECASE)
|
m = re.search("line\\s+(\\d+)", f, re.IGNORECASE)
|
||||||
if m and m.group(1):
|
if m and m.group(1):
|
||||||
lineText = m.group(1)
|
line_text = m.group(1)
|
||||||
else:
|
else:
|
||||||
lineText = 0
|
line_text = 0
|
||||||
|
|
||||||
sse = ScriptExecutionError()
|
sse = ScriptExecutionError()
|
||||||
sse.line = int(lineText)
|
sse.line = int(line_text)
|
||||||
sse.message = str(ex)
|
sse.message = str(ex)
|
||||||
raise sse
|
raise sse
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidParameterError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ScriptExecutionError(Exception):
|
class ScriptExecutionError(Exception):
|
||||||
"""
|
"""
|
||||||
Represents a script syntax error.
|
Represents a script syntax error.
|
||||||
Useful for helping clients pinpoint issues with the script
|
Useful for helping clients pinpoint issues with the script
|
||||||
interactively
|
interactively
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, line=None, message=None):
|
def __init__(self, line=None, message=None):
|
||||||
if line is None:
|
if line is None:
|
||||||
self.line = 0
|
self.line = 0
|
||||||
|
@ -197,7 +237,7 @@ class ScriptExecutionError(Exception):
|
||||||
else:
|
else:
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
def fullMessage(self):
|
def full_message(self):
|
||||||
return self.__repr__()
|
return self.__repr__()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -206,70 +246,48 @@ class ScriptExecutionError(Exception):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "ScriptError [Line %s]: %s" % (self.line, self.message)
|
return "ScriptError [Line %s]: %s" % (self.line, self.message)
|
||||||
|
|
||||||
class EnvironmentBuilder(object):
|
|
||||||
|
|
||||||
|
class EnvironmentBuilder(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.env = {}
|
self.env = {}
|
||||||
|
|
||||||
def withRealBuiltins(self):
|
def with_real_builtins(self):
|
||||||
return self.withBuiltins(__builtins__)
|
return self.with_builtins(__builtins__)
|
||||||
|
|
||||||
def withBuiltins(self,dict):
|
def with_builtins(self, env_dict):
|
||||||
self.env['__builtins__'] = dict
|
self.env['__builtins__'] = env_dict
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def addEntry(self,name, value):
|
def add_entry(self, name, value):
|
||||||
self.env[name] = value
|
self.env[name] = value
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
return self.env
|
return self.env
|
||||||
|
|
||||||
class VariableAssignmentChanger(ast.NodeTransformer):
|
|
||||||
"""
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, overrides):
|
|
||||||
self.overrides = overrides
|
|
||||||
|
|
||||||
def modify_node(self, varname, node):
|
|
||||||
if self.overrides.has_key(varname):
|
|
||||||
new_value = self.overrides[varname]
|
|
||||||
if type(node) == ast.Num:
|
|
||||||
node.n = new_value
|
|
||||||
elif type(node) == ast.Str:
|
|
||||||
node.s = new_value
|
|
||||||
else:
|
|
||||||
raise ValueError("Unknown Assignment Type:" + str(type(node)))
|
|
||||||
|
|
||||||
def visit_Assign(self, node):
|
|
||||||
left_side = node.targets[0]
|
|
||||||
if type(node.value) in [ast.Num, ast.Str]:
|
|
||||||
self.modify_node(left_side.id, node.value)
|
|
||||||
elif type(node.value) == ast.Tuple:
|
|
||||||
# we have a multi-value assignment
|
|
||||||
for n, v in zip(left_side.elts, node.value.elts):
|
|
||||||
self.modify_node(n.id, v)
|
|
||||||
return node
|
|
||||||
|
|
||||||
class ConstantAssignmentFinder(ast.NodeTransformer):
|
class ConstantAssignmentFinder(ast.NodeTransformer):
|
||||||
"""
|
"""
|
||||||
Visits a parse tree, and adds script parameters to the cqModel
|
Visits a parse tree, and adds script parameters to the cqModel
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,cqModel):
|
def __init__(self, cq_model):
|
||||||
self.cqModel = cqModel
|
self.cqModel = cq_model
|
||||||
|
|
||||||
def handle_assignment(self,varname, valuenode):
|
def handle_assignment(self, var_name, value_node):
|
||||||
if type(valuenode) == ast.Num:
|
if type(value_node) == ast.Num:
|
||||||
self.cqModel.add_script_parameter(InputParameter.create(valuenode,varname,NumberParameterType,valuenode.n))
|
self.cqModel.add_script_parameter(
|
||||||
elif type(valuenode) == ast.Str:
|
InputParameter.create(value_node, var_name, NumberParameterType, value_node.n))
|
||||||
self.cqModel.add_script_parameter(InputParameter.create(valuenode,varname,StringParameterType,valuenode.s))
|
elif type(value_node) == ast.Str:
|
||||||
elif type(valuenode == ast.Name):
|
self.cqModel.add_script_parameter(
|
||||||
if valuenode.value.Id == 'True':
|
InputParameter.create(value_node, var_name, StringParameterType, value_node.s))
|
||||||
self.cqModel.add_script_parameter(InputParameter.create(valuenode,varname,BooleanParameterType,True))
|
elif type(value_node == ast.Name):
|
||||||
elif valuenode.value.Id == 'False':
|
if value_node.value.Id == 'True':
|
||||||
self.cqModel.add_script_parameter(InputParameter.create(valuenode,varname,BooleanParameterType,True))
|
self.cqModel.add_script_parameter(
|
||||||
|
InputParameter.create(value_node, var_name, BooleanParameterType, True))
|
||||||
|
elif value_node.value.Id == 'False':
|
||||||
|
self.cqModel.add_script_parameter(
|
||||||
|
InputParameter.create(value_node, var_name, BooleanParameterType, True))
|
||||||
|
|
||||||
def visit_Assign(self, node):
|
def visit_Assign(self, node):
|
||||||
left_side = node.targets[0]
|
left_side = node.targets[0]
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
"""
|
"""
|
||||||
Tests CQGI functionality
|
Tests CQGI functionality
|
||||||
"""
|
|
||||||
|
|
||||||
|
Currently, this includes:
|
||||||
|
Parsing a script, and detecting its available variables
|
||||||
|
Altering the values at runtime
|
||||||
|
defining a build_object function to return results
|
||||||
|
"""
|
||||||
|
|
||||||
from cadquery import cqgi
|
from cadquery import cqgi
|
||||||
from tests import BaseTest
|
from tests import BaseTest
|
||||||
|
import textwrap
|
||||||
|
|
||||||
TESTSCRIPT = """
|
TESTSCRIPT = textwrap.dedent(
|
||||||
|
"""
|
||||||
height=2.0
|
height=2.0
|
||||||
width=3.0
|
width=3.0
|
||||||
(a,b) = (1.0,1.0)
|
(a,b) = (1.0,1.0)
|
||||||
|
@ -15,18 +21,20 @@ foo="bar"
|
||||||
result = "%s|%s|%s|%s" % ( str(height) , str(width) , foo , str(a) )
|
result = "%s|%s|%s|%s" % ( str(height) , str(width) , foo , str(a) )
|
||||||
build_object(result)
|
build_object(result)
|
||||||
"""
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestCQGI(BaseTest):
|
class TestCQGI(BaseTest):
|
||||||
|
|
||||||
|
|
||||||
def test_parser(self):
|
def test_parser(self):
|
||||||
model = cqgi.CQModel(TESTSCRIPT)
|
model = cqgi.CQModel(TESTSCRIPT)
|
||||||
metadata = model.metadata
|
metadata = model.metadata
|
||||||
self.assertEquals( len(metadata.parameters) , 5 )
|
|
||||||
|
self.assertEquals(set(metadata.parameters.keys()), {'height', 'width', 'a', 'b', 'foo'})
|
||||||
|
|
||||||
def test_build_with_empty_params(self):
|
def test_build_with_empty_params(self):
|
||||||
model = cqgi.CQModel(TESTSCRIPT)
|
model = cqgi.CQModel(TESTSCRIPT)
|
||||||
result = model.build({}) #building with no params should have no affect on the output
|
result = model.build()
|
||||||
|
|
||||||
self.assertTrue(result.success)
|
self.assertTrue(result.success)
|
||||||
self.assertTrue(len(result.results) == 1)
|
self.assertTrue(len(result.results) == 1)
|
||||||
self.assertTrue(result.results[0] == "2.0|3.0|bar|1.0")
|
self.assertTrue(result.results[0] == "2.0|3.0|bar|1.0")
|
||||||
|
@ -35,3 +43,79 @@ class TestCQGI(BaseTest):
|
||||||
model = cqgi.CQModel(TESTSCRIPT)
|
model = cqgi.CQModel(TESTSCRIPT)
|
||||||
result = model.build({'height': 3.0})
|
result = model.build({'height': 3.0})
|
||||||
self.assertTrue(result.results[0] == "3.0|3.0|bar|1.0")
|
self.assertTrue(result.results[0] == "3.0|3.0|bar|1.0")
|
||||||
|
|
||||||
|
def test_build_with_exception(self):
|
||||||
|
badscript = textwrap.dedent(
|
||||||
|
"""
|
||||||
|
raise ValueError("ERROR")
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
model = cqgi.CQModel(badscript)
|
||||||
|
result = model.build({})
|
||||||
|
self.assertFalse(result.success)
|
||||||
|
self.assertIsNotNone(result.exception)
|
||||||
|
self.assertTrue(result.exception.message == "ERROR")
|
||||||
|
|
||||||
|
def test_that_invalid_syntax_in_script_fails_immediately(self):
|
||||||
|
badscript = textwrap.dedent(
|
||||||
|
"""
|
||||||
|
this doesnt even compile
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as context:
|
||||||
|
model = cqgi.CQModel(badscript)
|
||||||
|
|
||||||
|
self.assertTrue('invalid syntax' in context.exception)
|
||||||
|
|
||||||
|
def test_that_two_results_are_returned(self):
|
||||||
|
script = textwrap.dedent(
|
||||||
|
"""
|
||||||
|
h = 1
|
||||||
|
build_object(h)
|
||||||
|
h = 2
|
||||||
|
build_object(h)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
model = cqgi.CQModel(script)
|
||||||
|
result = model.build({})
|
||||||
|
self.assertEquals(2, len(result.results))
|
||||||
|
self.assertEquals(1, result.results[0])
|
||||||
|
self.assertEquals(2, result.results[1])
|
||||||
|
|
||||||
|
def test_that_assinging_number_to_string_works(self):
|
||||||
|
script = textwrap.dedent(
|
||||||
|
"""
|
||||||
|
h = "this is a string"
|
||||||
|
build_object(h)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = self._executeScriptWithParams(script, {'h': 33.33})
|
||||||
|
self.assertEquals(result.results[0], "33.33")
|
||||||
|
|
||||||
|
def test_that_assigning_string_to_number_fails(self):
|
||||||
|
script = textwrap.dedent(
|
||||||
|
"""
|
||||||
|
h = 20.0
|
||||||
|
build_object(h)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
result = self._executeScriptWithParams(script, {'h': "a string"})
|
||||||
|
|
||||||
|
def test_that_assigning_unknown_var_fails(self):
|
||||||
|
|
||||||
|
script = textwrap.dedent(
|
||||||
|
"""
|
||||||
|
h = 20.0
|
||||||
|
build_object(h)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
with self.assertRaises(cqgi.InvalidParameterError):
|
||||||
|
result = self._executeScriptWithParams(script, {'w': "var is not there"})
|
||||||
|
|
||||||
|
def _executeScriptWithParams(self, script, params):
|
||||||
|
model = cqgi.CQModel(script)
|
||||||
|
return model.build(params)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user