mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-01-24 15:26:46 -06:00
Fixed cognitive complexity issues reported by SonarQube.
This commit is contained in:
parent
aa679e06b2
commit
6a406f466d
@ -176,6 +176,65 @@ class DomainView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare):
|
||||
|
||||
keys_to_ignore = ['oid', 'basensp', 'conoid', 'nspname', 'oid-2']
|
||||
|
||||
@staticmethod
|
||||
def _get_req_data(kwargs):
|
||||
"""
|
||||
Get req data from request.
|
||||
:param kwargs: kwargs.
|
||||
:return: if any error return error, else return req.
|
||||
"""
|
||||
if request.data:
|
||||
req = json.loads(request.data, encoding='utf-8')
|
||||
else:
|
||||
req = request.args or request.form
|
||||
|
||||
if 'doid' not in kwargs:
|
||||
required_args = [
|
||||
'name',
|
||||
'basetype'
|
||||
]
|
||||
|
||||
for arg in required_args:
|
||||
if arg not in req or req[arg] == '':
|
||||
return req, True, make_json_response(
|
||||
status=410,
|
||||
success=0,
|
||||
errormsg=gettext(
|
||||
"Could not find the required parameter ({})."
|
||||
).format(arg),
|
||||
)
|
||||
return req, False, ''
|
||||
|
||||
@staticmethod
|
||||
def _get_data(req):
|
||||
"""
|
||||
Get data from request and update required values.
|
||||
:param req: request object.
|
||||
:return: data.
|
||||
"""
|
||||
data = {}
|
||||
list_params = []
|
||||
if request.method == 'GET':
|
||||
list_params = ['constraints', 'seclabels']
|
||||
|
||||
for key in req:
|
||||
if (
|
||||
key in list_params and req[key] != '' and
|
||||
req[key] is not None
|
||||
):
|
||||
# Coverts string into python list as expected.
|
||||
data[key] = json.loads(req[key], encoding='utf-8')
|
||||
elif key == 'typnotnull':
|
||||
if req[key] == 'true' or req[key] is True:
|
||||
data[key] = True
|
||||
elif req[key] == 'false' or req[key] is False:
|
||||
data[key] = False
|
||||
else:
|
||||
data[key] = ''
|
||||
else:
|
||||
data[key] = req[key]
|
||||
return data
|
||||
|
||||
def validate_request(f):
|
||||
"""
|
||||
Works as a decorator.
|
||||
@ -193,49 +252,12 @@ class DomainView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare):
|
||||
@wraps(f)
|
||||
def wrap(self, **kwargs):
|
||||
|
||||
data = {}
|
||||
if request.data:
|
||||
req = json.loads(request.data, encoding='utf-8')
|
||||
else:
|
||||
req = request.args or request.form
|
||||
|
||||
if 'doid' not in kwargs:
|
||||
required_args = [
|
||||
'name',
|
||||
'basetype'
|
||||
]
|
||||
|
||||
for arg in required_args:
|
||||
if arg not in req or req[arg] == '':
|
||||
return make_json_response(
|
||||
status=410,
|
||||
success=0,
|
||||
errormsg=gettext(
|
||||
"Could not find the required parameter ({})."
|
||||
).format(arg)
|
||||
)
|
||||
req, is_error, errmsg = DomainView._get_req_data(kwargs)
|
||||
if is_error:
|
||||
return errmsg
|
||||
|
||||
try:
|
||||
list_params = []
|
||||
if request.method == 'GET':
|
||||
list_params = ['constraints', 'seclabels']
|
||||
|
||||
for key in req:
|
||||
if (
|
||||
key in list_params and req[key] != '' and
|
||||
req[key] is not None
|
||||
):
|
||||
# Coverts string into python list as expected.
|
||||
data[key] = json.loads(req[key], encoding='utf-8')
|
||||
elif key == 'typnotnull':
|
||||
if req[key] == 'true' or req[key] is True:
|
||||
data[key] = True
|
||||
elif req[key] == 'false' or req[key] is False:
|
||||
data[key] = False
|
||||
else:
|
||||
data[key] = ''
|
||||
else:
|
||||
data[key] = req[key]
|
||||
data = DomainView._get_data(req)
|
||||
|
||||
except Exception as e:
|
||||
return internal_server_error(errormsg=str(e))
|
||||
|
@ -179,6 +179,35 @@ class DomainConstraintView(PGChildNodeView):
|
||||
'dependent': [{'get': 'dependents'}]
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def _get_req_data(kwargs):
|
||||
"""
|
||||
Get data from request.
|
||||
:param kwargs: kwargs for request.
|
||||
:return: if any error return error with error msg else return req data
|
||||
"""
|
||||
if request.data:
|
||||
req = json.loads(request.data, encoding='utf-8')
|
||||
else:
|
||||
req = request.args or request.form
|
||||
|
||||
if 'coid' not in kwargs:
|
||||
required_args = [
|
||||
'name',
|
||||
'consrc'
|
||||
]
|
||||
|
||||
for arg in required_args:
|
||||
if arg not in req or req[arg] == '':
|
||||
return True, make_json_response(
|
||||
status=410,
|
||||
success=0,
|
||||
errormsg=gettext(
|
||||
"Could not find the required parameter ({})."
|
||||
).format(arg)
|
||||
), req
|
||||
return False, '', req
|
||||
|
||||
def validate_request(f):
|
||||
"""
|
||||
Works as a decorator.
|
||||
@ -195,26 +224,9 @@ class DomainConstraintView(PGChildNodeView):
|
||||
def wrap(self, **kwargs):
|
||||
|
||||
data = {}
|
||||
if request.data:
|
||||
req = json.loads(request.data, encoding='utf-8')
|
||||
else:
|
||||
req = request.args or request.form
|
||||
|
||||
if 'coid' not in kwargs:
|
||||
required_args = [
|
||||
'name',
|
||||
'consrc'
|
||||
]
|
||||
|
||||
for arg in required_args:
|
||||
if arg not in req or req[arg] == '':
|
||||
return make_json_response(
|
||||
status=410,
|
||||
success=0,
|
||||
errormsg=gettext(
|
||||
"Could not find the required parameter ({})."
|
||||
).format(arg)
|
||||
)
|
||||
is_error, errmsg, req = DomainConstraintView._get_req_data(kwargs)
|
||||
if is_error:
|
||||
return errmsg
|
||||
|
||||
try:
|
||||
for key in req:
|
||||
|
@ -583,23 +583,102 @@ class ColumnsView(PGChildNodeView, DataTypeReader):
|
||||
except Exception as e:
|
||||
return internal_server_error(errormsg=str(e))
|
||||
|
||||
def _parse_acl_to_db_parsing(self, data, old_data):
|
||||
"""
|
||||
Convert acl coming from client to required db parsing format.
|
||||
:param data: Data.
|
||||
:param old_data: old data for comparision and get name.
|
||||
"""
|
||||
# If name is not present in data then
|
||||
# we will fetch it from old data, we also need schema & table name
|
||||
if 'name' not in data:
|
||||
data['name'] = old_data['name']
|
||||
|
||||
# Convert acl coming from client in db parsing format
|
||||
key = 'attacl'
|
||||
if key in data and data[key] is not None:
|
||||
if 'added' in data[key]:
|
||||
data[key]['added'] = parse_priv_to_db(
|
||||
data[key]['added'], self.acl
|
||||
)
|
||||
if 'changed' in data[key]:
|
||||
data[key]['changed'] = parse_priv_to_db(
|
||||
data[key]['changed'], self.acl
|
||||
)
|
||||
if 'deleted' in data[key]:
|
||||
data[key]['deleted'] = parse_priv_to_db(
|
||||
data[key]['deleted'], self.acl
|
||||
)
|
||||
|
||||
def _get_sql_for_create(self, data, is_sql):
|
||||
"""
|
||||
Get sql for create column model.
|
||||
:param data: Data.
|
||||
:param is_sql: flag for get sql.
|
||||
:return: if any error return error else return sql.
|
||||
"""
|
||||
required_args = [
|
||||
'name',
|
||||
'cltype'
|
||||
]
|
||||
|
||||
for arg in required_args:
|
||||
if arg not in data:
|
||||
return True, gettext('-- definition incomplete'), ''
|
||||
|
||||
# We will convert privileges coming from client required
|
||||
# in server side format
|
||||
if 'attacl' in data:
|
||||
data['attacl'] = parse_priv_to_db(data['attacl'],
|
||||
self.acl)
|
||||
# If the request for new object which do not have did
|
||||
sql = render_template(
|
||||
"/".join([self.template_path, self._CREATE_SQL]),
|
||||
data=data, conn=self.conn, is_sql=is_sql
|
||||
)
|
||||
|
||||
return False, '', sql
|
||||
|
||||
def _check_type(self, data, old_data):
|
||||
"""
|
||||
Check cltype and get required data form it.
|
||||
:param data: Data.
|
||||
:param old_data: old data for check and get default values.
|
||||
"""
|
||||
# check type for '[]' in it
|
||||
if 'cltype' in old_data:
|
||||
old_data['cltype'], old_data['hasSqrBracket'] = \
|
||||
column_utils.type_formatter(old_data['cltype'])
|
||||
|
||||
if 'cltype' in data and data['cltype'] != old_data['cltype']:
|
||||
length, precision, typeval = \
|
||||
self.get_length_precision(data['cltype'])
|
||||
|
||||
# if new datatype does not have length or precision
|
||||
# then we cannot apply length or precision of old
|
||||
# datatype to new one.
|
||||
if not length:
|
||||
old_data['attlen'] = -1
|
||||
if not precision:
|
||||
old_data['attprecision'] = None
|
||||
|
||||
def get_sql(self, scid, tid, clid, data, is_sql=False):
|
||||
"""
|
||||
This function will genrate sql from model data
|
||||
This function will generate sql from model data
|
||||
"""
|
||||
data = column_utils.convert_length_precision_to_string(data)
|
||||
|
||||
if clid is not None:
|
||||
SQL = render_template(
|
||||
sql = render_template(
|
||||
"/".join([self.template_path, self._PROPERTIES_SQL]),
|
||||
tid=tid, clid=clid,
|
||||
show_sys_objects=self.blueprint.show_system_objects
|
||||
)
|
||||
|
||||
status, res = self.conn.execute_dict(SQL)
|
||||
status, res = self.conn.execute_dict(sql)
|
||||
if not status:
|
||||
return internal_server_error(errormsg=res)
|
||||
if len(res['rows']) == 0:
|
||||
elif len(res['rows']) == 0:
|
||||
return gone(
|
||||
gettext("Could not find the column on the server.")
|
||||
)
|
||||
@ -610,69 +689,19 @@ class ColumnsView(PGChildNodeView, DataTypeReader):
|
||||
old_data = column_utils.column_formatter(
|
||||
self.conn, tid, clid, old_data)
|
||||
|
||||
# check type for '[]' in it
|
||||
if 'cltype' in old_data:
|
||||
old_data['cltype'], old_data['hasSqrBracket'] = \
|
||||
column_utils.type_formatter(old_data['cltype'])
|
||||
self._check_type(data, old_data)
|
||||
self._parse_acl_to_db_parsing(data, old_data)
|
||||
|
||||
if 'cltype' in data and data['cltype'] != old_data['cltype']:
|
||||
length, precision, typeval = \
|
||||
self.get_length_precision(data['cltype'])
|
||||
|
||||
# if new datatype does not have length or precision
|
||||
# then we cannot apply length or precision of old
|
||||
# datatype to new one.
|
||||
if not length:
|
||||
old_data['attlen'] = -1
|
||||
if not precision:
|
||||
old_data['attprecision'] = None
|
||||
|
||||
# If name is not present in data then
|
||||
# we will fetch it from old data, we also need schema & table name
|
||||
if 'name' not in data:
|
||||
data['name'] = old_data['name']
|
||||
|
||||
# Convert acl coming from client in db parsing format
|
||||
key = 'attacl'
|
||||
if key in data and data[key] is not None:
|
||||
if 'added' in data[key]:
|
||||
data[key]['added'] = parse_priv_to_db(
|
||||
data[key]['added'], self.acl
|
||||
)
|
||||
if 'changed' in data[key]:
|
||||
data[key]['changed'] = parse_priv_to_db(
|
||||
data[key]['changed'], self.acl
|
||||
)
|
||||
if 'deleted' in data[key]:
|
||||
data[key]['deleted'] = parse_priv_to_db(
|
||||
data[key]['deleted'], self.acl
|
||||
)
|
||||
|
||||
SQL = render_template(
|
||||
sql = render_template(
|
||||
"/".join([self.template_path, self._UPDATE_SQL]),
|
||||
data=data, o_data=old_data, conn=self.conn
|
||||
)
|
||||
else:
|
||||
required_args = [
|
||||
'name',
|
||||
'cltype'
|
||||
]
|
||||
is_error, errmsg, sql = self._get_sql_for_create(data, is_sql)
|
||||
if is_error:
|
||||
return errmsg
|
||||
|
||||
for arg in required_args:
|
||||
if arg not in data:
|
||||
return gettext('-- definition incomplete')
|
||||
|
||||
# We will convert privileges coming from client required
|
||||
# in server side format
|
||||
if 'attacl' in data:
|
||||
data['attacl'] = parse_priv_to_db(data['attacl'],
|
||||
self.acl)
|
||||
# If the request for new object which do not have did
|
||||
SQL = render_template(
|
||||
"/".join([self.template_path, self._CREATE_SQL]),
|
||||
data=data, conn=self.conn, is_sql=is_sql
|
||||
)
|
||||
return SQL, data['name'] if 'name' in data else old_data['name']
|
||||
return sql, data['name'] if 'name' in data else old_data['name']
|
||||
|
||||
@check_precondition
|
||||
def sql(self, gid, sid, did, scid, tid, clid):
|
||||
|
@ -241,6 +241,53 @@ def get_formatted_columns(conn, tid, data, other_columns,
|
||||
return data
|
||||
|
||||
|
||||
def _parse_column_actions(final_columns, column_acl):
|
||||
"""
|
||||
Check action and access for it.
|
||||
:param final_columns: final column list
|
||||
:param column_acl: Column access.
|
||||
"""
|
||||
for c in final_columns:
|
||||
if 'attacl' in c:
|
||||
if 'added' in c['attacl']:
|
||||
c['attacl']['added'] = parse_priv_to_db(
|
||||
c['attacl']['added'], column_acl
|
||||
)
|
||||
elif 'changed' in c['attacl']:
|
||||
c['attacl']['changed'] = parse_priv_to_db(
|
||||
c['attacl']['changed'], column_acl
|
||||
)
|
||||
elif 'deleted' in c['attacl']:
|
||||
c['attacl']['deleted'] = parse_priv_to_db(
|
||||
c['attacl']['deleted'], column_acl
|
||||
)
|
||||
if 'cltype' in c:
|
||||
# check type for '[]' in it
|
||||
c['cltype'], c['hasSqrBracket'] = \
|
||||
type_formatter(c['cltype'])
|
||||
|
||||
c = convert_length_precision_to_string(c)
|
||||
|
||||
|
||||
def _parse_format_col_for_edit(data, columns, column_acl):
|
||||
"""
|
||||
This function parser columns for edit mode.
|
||||
:param data: Data from req.
|
||||
:param columns: Columns list from data
|
||||
:param column_acl: Column access.
|
||||
"""
|
||||
for action in ['added', 'changed']:
|
||||
if action in columns:
|
||||
final_columns = []
|
||||
for c in columns[action]:
|
||||
if 'inheritedfrom' not in c:
|
||||
final_columns.append(c)
|
||||
|
||||
_parse_column_actions(final_columns, column_acl)
|
||||
|
||||
data['columns'][action] = final_columns
|
||||
|
||||
|
||||
def parse_format_columns(data, mode=None):
|
||||
"""
|
||||
This function will parse and return formatted list of columns
|
||||
@ -254,35 +301,7 @@ def parse_format_columns(data, mode=None):
|
||||
columns = data['columns']
|
||||
# 'EDIT' mode
|
||||
if mode is not None:
|
||||
for action in ['added', 'changed']:
|
||||
if action in columns:
|
||||
final_columns = []
|
||||
for c in columns[action]:
|
||||
if 'inheritedfrom' not in c:
|
||||
final_columns.append(c)
|
||||
|
||||
for c in final_columns:
|
||||
if 'attacl' in c:
|
||||
if 'added' in c['attacl']:
|
||||
c['attacl']['added'] = parse_priv_to_db(
|
||||
c['attacl']['added'], column_acl
|
||||
)
|
||||
elif 'changed' in c['attacl']:
|
||||
c['attacl']['changed'] = parse_priv_to_db(
|
||||
c['attacl']['changed'], column_acl
|
||||
)
|
||||
elif 'deleted' in c['attacl']:
|
||||
c['attacl']['deleted'] = parse_priv_to_db(
|
||||
c['attacl']['deleted'], column_acl
|
||||
)
|
||||
if 'cltype' in c:
|
||||
# check type for '[]' in it
|
||||
c['cltype'], c['hasSqrBracket'] = \
|
||||
type_formatter(c['cltype'])
|
||||
|
||||
c = convert_length_precision_to_string(c)
|
||||
|
||||
data['columns'][action] = final_columns
|
||||
_parse_format_col_for_edit(data, columns, column_acl)
|
||||
else:
|
||||
# We need to exclude all the columns which are inherited from other
|
||||
# tables 'CREATE' mode
|
||||
|
@ -59,6 +59,31 @@ def get_parent(conn, tid, template_path=None):
|
||||
return schema, table
|
||||
|
||||
|
||||
def _get_columns(res):
|
||||
"""
|
||||
Get columns form response and return in required format.
|
||||
:param res: response form constraints.
|
||||
:return: column list.
|
||||
"""
|
||||
columns = []
|
||||
for row in res['rows']:
|
||||
if row['options'] & 1:
|
||||
order = False
|
||||
nulls_order = True if (row['options'] & 2) else False
|
||||
else:
|
||||
order = True
|
||||
nulls_order = True if (row['options'] & 2) else False
|
||||
|
||||
columns.append({"column": row['coldef'].strip('"'),
|
||||
"oper_class": row['opcname'],
|
||||
"order": order,
|
||||
"nulls_order": nulls_order,
|
||||
"operator": row['oprname'],
|
||||
"col_type": row['datatype']
|
||||
})
|
||||
return columns
|
||||
|
||||
|
||||
@get_template_path
|
||||
def get_exclusion_constraints(conn, did, tid, exid=None, template_path=None):
|
||||
"""
|
||||
@ -87,22 +112,7 @@ def get_exclusion_constraints(conn, did, tid, exid=None, template_path=None):
|
||||
if not status:
|
||||
return status, internal_server_error(errormsg=res)
|
||||
|
||||
columns = []
|
||||
for row in res['rows']:
|
||||
if row['options'] & 1:
|
||||
order = False
|
||||
nulls_order = True if (row['options'] & 2) else False
|
||||
else:
|
||||
order = True
|
||||
nulls_order = True if (row['options'] & 2) else False
|
||||
|
||||
columns.append({"column": row['coldef'].strip('"'),
|
||||
"oper_class": row['opcname'],
|
||||
"order": order,
|
||||
"nulls_order": nulls_order,
|
||||
"operator": row['oprname'],
|
||||
"col_type": row['datatype']
|
||||
})
|
||||
columns = _get_columns(res)
|
||||
|
||||
ex['columns'] = columns
|
||||
|
||||
@ -120,6 +130,28 @@ def get_exclusion_constraints(conn, did, tid, exid=None, template_path=None):
|
||||
return True, result['rows']
|
||||
|
||||
|
||||
def _get_delete_constraint(data, constraint, sql, template_path, conn):
|
||||
"""
|
||||
Check for delete constraints and return sql for it.
|
||||
:param data: Data req.
|
||||
:param constraint: Constraint list for check.
|
||||
:param sql: sql list to append delete constraint sql.
|
||||
:param template_path: Template path to fetch sql for delete constraint.
|
||||
:param conn: Connection.
|
||||
"""
|
||||
if 'deleted' in constraint:
|
||||
for c in constraint['deleted']:
|
||||
c['schema'] = data['schema']
|
||||
c['table'] = data['name']
|
||||
|
||||
# Sql for drop
|
||||
sql.append(
|
||||
render_template("/".join(
|
||||
[template_path, 'delete.sql']),
|
||||
data=c, conn=conn).strip("\n")
|
||||
)
|
||||
|
||||
|
||||
@get_template_path
|
||||
def get_exclusion_constraint_sql(conn, did, tid, data, template_path=None):
|
||||
"""
|
||||
@ -138,17 +170,7 @@ def get_exclusion_constraint_sql(conn, did, tid, data, template_path=None):
|
||||
if 'exclude_constraint' in data:
|
||||
constraint = data['exclude_constraint']
|
||||
# If constraint(s) is/are deleted
|
||||
if 'deleted' in constraint:
|
||||
for c in constraint['deleted']:
|
||||
c['schema'] = data['schema']
|
||||
c['table'] = data['name']
|
||||
|
||||
# Sql for drop
|
||||
sql.append(
|
||||
render_template("/".join(
|
||||
[template_path, 'delete.sql']),
|
||||
data=c, conn=conn).strip("\n")
|
||||
)
|
||||
_get_delete_constraint(data, constraint, sql, template_path, conn)
|
||||
|
||||
if 'changed' in constraint:
|
||||
for c in constraint['changed']:
|
||||
|
@ -1066,18 +1066,12 @@ class TypeView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare):
|
||||
except Exception as e:
|
||||
return internal_server_error(errormsg=str(e))
|
||||
|
||||
@check_precondition
|
||||
def delete(self, gid, sid, did, scid, tid=None, only_sql=False):
|
||||
def _get_req_delete_data(self, tid, only_sql):
|
||||
"""
|
||||
This function will updates existing the type object
|
||||
|
||||
Args:
|
||||
gid: Server Group ID
|
||||
sid: Server ID
|
||||
did: Database ID
|
||||
scid: Schema ID
|
||||
tid: Type ID
|
||||
only_sql: Return only sql if True
|
||||
This function get data from request
|
||||
:param tid: Table Id
|
||||
:param only_sql: Flag for sql only.
|
||||
:return: data and cascade flag.
|
||||
"""
|
||||
if tid is None:
|
||||
data = request.form if request.form else json.loads(
|
||||
@ -1093,16 +1087,33 @@ class TypeView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare):
|
||||
else:
|
||||
cascade = False
|
||||
|
||||
return data, cascade
|
||||
|
||||
@check_precondition
|
||||
def delete(self, gid, sid, did, scid, tid=None, only_sql=False):
|
||||
"""
|
||||
This function will updates existing the type object
|
||||
|
||||
Args:
|
||||
gid: Server Group ID
|
||||
sid: Server ID
|
||||
did: Database ID
|
||||
scid: Schema ID
|
||||
tid: Type ID
|
||||
only_sql: Return only sql if True
|
||||
"""
|
||||
data, cascade = self._get_req_delete_data(tid, only_sql)
|
||||
|
||||
try:
|
||||
for tid in data['ids']:
|
||||
SQL = render_template(
|
||||
sql = render_template(
|
||||
"/".join([self.template_path,
|
||||
self._PROPERTIES_SQL]),
|
||||
scid=scid, tid=tid,
|
||||
datlastsysoid=self.datlastsysoid,
|
||||
show_system_objects=self.blueprint.show_system_objects
|
||||
)
|
||||
status, res = self.conn.execute_dict(SQL)
|
||||
status, res = self.conn.execute_dict(sql)
|
||||
if not status:
|
||||
return internal_server_error(errormsg=res)
|
||||
|
||||
@ -1121,7 +1132,7 @@ class TypeView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare):
|
||||
# Making copy of output for future use
|
||||
data = dict(res['rows'][0])
|
||||
|
||||
SQL = render_template("/".join([self.template_path,
|
||||
sql = render_template("/".join([self.template_path,
|
||||
self._DELETE_SQL]),
|
||||
data=data,
|
||||
cascade=cascade,
|
||||
@ -1129,9 +1140,9 @@ class TypeView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare):
|
||||
|
||||
# Used for schema diff tool
|
||||
if only_sql:
|
||||
return SQL
|
||||
return sql
|
||||
|
||||
status, res = self.conn.execute_scalar(SQL)
|
||||
status, res = self.conn.execute_scalar(sql)
|
||||
if not status:
|
||||
return internal_server_error(errormsg=res)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user