mirror of
https://github.com/apache/superset.git
synced 2026-04-21 00:54:44 +00:00
[dashboard] New, add statsd metrics to the API (#9519)
This commit is contained in:
committed by
GitHub
parent
d9ebd32485
commit
7b11b44abe
@@ -18,10 +18,11 @@
|
||||
"""Unit tests for Superset"""
|
||||
import imp
|
||||
import json
|
||||
from typing import Union
|
||||
from unittest.mock import Mock
|
||||
from typing import Union, Dict
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pandas as pd
|
||||
from flask import Response
|
||||
from flask_appbuilder.security.sqla import models as ab_models
|
||||
from flask_testing import TestCase
|
||||
|
||||
@@ -35,6 +36,7 @@ from superset.models.core import Database
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.models.datasource_access_request import DatasourceAccessRequest
|
||||
from superset.utils.core import get_example_database
|
||||
from superset.views.base_api import BaseSupersetModelRestApi
|
||||
|
||||
FAKE_DB_NAME = "fake_db_100"
|
||||
|
||||
@@ -328,3 +330,81 @@ class SupersetTestCase(TestCase):
|
||||
def get_dash_by_slug(self, dash_slug):
|
||||
sesh = db.session()
|
||||
return sesh.query(Dashboard).filter_by(slug=dash_slug).first()
|
||||
|
||||
def get_assert_metric(self, uri: str, func_name: str) -> Response:
|
||||
"""
|
||||
Simple client get with an extra assertion for statsd metrics
|
||||
|
||||
:param uri: The URI to use for the HTTP GET
|
||||
:param func_name: The function name that the HTTP GET triggers
|
||||
for the statsd metric assertion
|
||||
:return: HTTP Response
|
||||
"""
|
||||
with patch.object(
|
||||
BaseSupersetModelRestApi, "incr_stats", return_value=None
|
||||
) as mock_method:
|
||||
rv = self.client.get(uri)
|
||||
if 200 <= rv.status_code < 400:
|
||||
mock_method.assert_called_once_with("success", func_name)
|
||||
else:
|
||||
mock_method.assert_called_once_with("error", func_name)
|
||||
return rv
|
||||
|
||||
def delete_assert_metric(self, uri: str, func_name: str) -> Response:
|
||||
"""
|
||||
Simple client delete with an extra assertion for statsd metrics
|
||||
|
||||
:param uri: The URI to use for the HTTP DELETE
|
||||
:param func_name: The function name that the HTTP DELETE triggers
|
||||
for the statsd metric assertion
|
||||
:return: HTTP Response
|
||||
"""
|
||||
with patch.object(
|
||||
BaseSupersetModelRestApi, "incr_stats", return_value=None
|
||||
) as mock_method:
|
||||
rv = self.client.delete(uri)
|
||||
if 200 <= rv.status_code < 400:
|
||||
mock_method.assert_called_once_with("success", func_name)
|
||||
else:
|
||||
mock_method.assert_called_once_with("error", func_name)
|
||||
return rv
|
||||
|
||||
def post_assert_metric(self, uri: str, data: Dict, func_name: str) -> Response:
|
||||
"""
|
||||
Simple client post with an extra assertion for statsd metrics
|
||||
|
||||
:param uri: The URI to use for the HTTP POST
|
||||
:param data: The JSON data payload to be posted
|
||||
:param func_name: The function name that the HTTP POST triggers
|
||||
for the statsd metric assertion
|
||||
:return: HTTP Response
|
||||
"""
|
||||
with patch.object(
|
||||
BaseSupersetModelRestApi, "incr_stats", return_value=None
|
||||
) as mock_method:
|
||||
rv = self.client.post(uri, json=data)
|
||||
if 200 <= rv.status_code < 400:
|
||||
mock_method.assert_called_once_with("success", func_name)
|
||||
else:
|
||||
mock_method.assert_called_once_with("error", func_name)
|
||||
return rv
|
||||
|
||||
def put_assert_metric(self, uri: str, data: Dict, func_name: str) -> Response:
|
||||
"""
|
||||
Simple client put with an extra assertion for statsd metrics
|
||||
|
||||
:param uri: The URI to use for the HTTP PUT
|
||||
:param data: The JSON data payload to be posted
|
||||
:param func_name: The function name that the HTTP PUT triggers
|
||||
for the statsd metric assertion
|
||||
:return: HTTP Response
|
||||
"""
|
||||
with patch.object(
|
||||
BaseSupersetModelRestApi, "incr_stats", return_value=None
|
||||
) as mock_method:
|
||||
rv = self.client.put(uri, json=data)
|
||||
if 200 <= rv.status_code < 400:
|
||||
mock_method.assert_called_once_with("success", func_name)
|
||||
else:
|
||||
mock_method.assert_called_once_with("error", func_name)
|
||||
return rv
|
||||
|
||||
@@ -79,13 +79,13 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_get_dashboard(self):
|
||||
"""
|
||||
Dashboard API: Test get dashboard
|
||||
Dashboard API: Test get dashboard
|
||||
"""
|
||||
admin = self.get_user("admin")
|
||||
dashboard = self.insert_dashboard("title", "slug1", [admin.id])
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/{dashboard.id}"
|
||||
rv = self.client.get(uri)
|
||||
rv = self.get_assert_metric(uri, "get")
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
expected_result = {
|
||||
"changed_by": None,
|
||||
@@ -121,19 +121,28 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
db.session.delete(dashboard)
|
||||
db.session.commit()
|
||||
|
||||
def test_info_dashboard(self):
|
||||
"""
|
||||
Dashboard API: Test info
|
||||
"""
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/_info"
|
||||
rv = self.get_assert_metric(uri, "info")
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
|
||||
def test_get_dashboard_not_found(self):
|
||||
"""
|
||||
Dashboard API: Test get dashboard not found
|
||||
Dashboard API: Test get dashboard not found
|
||||
"""
|
||||
max_id = db.session.query(func.max(Dashboard.id)).scalar()
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/{max_id + 1}"
|
||||
rv = self.client.get(uri)
|
||||
rv = self.get_assert_metric(uri, "get")
|
||||
self.assertEqual(rv.status_code, 404)
|
||||
|
||||
def test_get_dashboard_no_data_access(self):
|
||||
"""
|
||||
Dashboard API: Test get dashboard without data access
|
||||
Dashboard API: Test get dashboard without data access
|
||||
"""
|
||||
admin = self.get_user("admin")
|
||||
dashboard = self.insert_dashboard("title", "slug1", [admin.id])
|
||||
@@ -148,7 +157,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_get_dashboards_filter(self):
|
||||
"""
|
||||
Dashboard API: Test get dashboards filter
|
||||
Dashboard API: Test get dashboards filter
|
||||
"""
|
||||
admin = self.get_user("admin")
|
||||
gamma = self.get_user("gamma")
|
||||
@@ -160,7 +169,8 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
"filters": [{"col": "dashboard_title", "opr": "sw", "value": "ti"}]
|
||||
}
|
||||
uri = f"api/v1/dashboard/?q={prison.dumps(arguments)}"
|
||||
rv = self.client.get(uri)
|
||||
|
||||
rv = self.get_assert_metric(uri, "get_list")
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
self.assertEqual(data["count"], 1)
|
||||
@@ -182,7 +192,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_get_dashboards_custom_filter(self):
|
||||
"""
|
||||
Dashboard API: Test get dashboards custom filter
|
||||
Dashboard API: Test get dashboards custom filter
|
||||
"""
|
||||
admin = self.get_user("admin")
|
||||
dashboard1 = self.insert_dashboard("foo", "ZY_bar", [admin.id])
|
||||
@@ -232,7 +242,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_get_dashboards_no_data_access(self):
|
||||
"""
|
||||
Dashboard API: Test get dashboards no data access
|
||||
Dashboard API: Test get dashboards no data access
|
||||
"""
|
||||
admin = self.get_user("admin")
|
||||
dashboard = self.insert_dashboard("title", "slug1", [admin.id])
|
||||
@@ -253,20 +263,20 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_delete_dashboard(self):
|
||||
"""
|
||||
Dashboard API: Test delete
|
||||
Dashboard API: Test delete
|
||||
"""
|
||||
admin_id = self.get_user("admin").id
|
||||
dashboard_id = self.insert_dashboard("title", "slug1", [admin_id]).id
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/{dashboard_id}"
|
||||
rv = self.client.delete(uri)
|
||||
rv = self.delete_assert_metric(uri, "delete")
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
model = db.session.query(Dashboard).get(dashboard_id)
|
||||
self.assertEqual(model, None)
|
||||
|
||||
def test_delete_bulk_dashboards(self):
|
||||
"""
|
||||
Dashboard API: Test delete bulk
|
||||
Dashboard API: Test delete bulk
|
||||
"""
|
||||
admin_id = self.get_user("admin").id
|
||||
dashboard_count = 4
|
||||
@@ -282,7 +292,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
self.login(username="admin")
|
||||
argument = dashboard_ids
|
||||
uri = f"api/v1/dashboard/?q={prison.dumps(argument)}"
|
||||
rv = self.client.delete(uri)
|
||||
rv = self.delete_assert_metric(uri, "bulk_delete")
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
response = json.loads(rv.data.decode("utf-8"))
|
||||
expected_response = {"message": f"Deleted {dashboard_count} dashboards"}
|
||||
@@ -293,7 +303,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_delete_bulk_dashboards_bad_request(self):
|
||||
"""
|
||||
Dashboard API: Test delete bulk bad request
|
||||
Dashboard API: Test delete bulk bad request
|
||||
"""
|
||||
dashboard_ids = [1, "a"]
|
||||
self.login(username="admin")
|
||||
@@ -304,7 +314,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_delete_not_found_dashboard(self):
|
||||
"""
|
||||
Dashboard API: Test not found delete
|
||||
Dashboard API: Test not found delete
|
||||
"""
|
||||
self.login(username="admin")
|
||||
dashboard_id = 1000
|
||||
@@ -314,7 +324,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_delete_bulk_dashboards_not_found(self):
|
||||
"""
|
||||
Dashboard API: Test delete bulk not found
|
||||
Dashboard API: Test delete bulk not found
|
||||
"""
|
||||
dashboard_ids = [1001, 1002]
|
||||
self.login(username="admin")
|
||||
@@ -325,7 +335,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_delete_dashboard_admin_not_owned(self):
|
||||
"""
|
||||
Dashboard API: Test admin delete not owned
|
||||
Dashboard API: Test admin delete not owned
|
||||
"""
|
||||
gamma_id = self.get_user("gamma").id
|
||||
dashboard_id = self.insert_dashboard("title", "slug1", [gamma_id]).id
|
||||
@@ -339,7 +349,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_delete_bulk_dashboard_admin_not_owned(self):
|
||||
"""
|
||||
Dashboard API: Test admin delete bulk not owned
|
||||
Dashboard API: Test admin delete bulk not owned
|
||||
"""
|
||||
gamma_id = self.get_user("gamma").id
|
||||
dashboard_count = 4
|
||||
@@ -368,7 +378,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_delete_dashboard_not_owned(self):
|
||||
"""
|
||||
Dashboard API: Test delete try not owned
|
||||
Dashboard API: Test delete try not owned
|
||||
"""
|
||||
user_alpha1 = self.create_user(
|
||||
"alpha1", "password", "Alpha", email="alpha1@superset.org"
|
||||
@@ -393,7 +403,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_delete_bulk_dashboard_not_owned(self):
|
||||
"""
|
||||
Dashboard API: Test delete bulk try not owned
|
||||
Dashboard API: Test delete bulk try not owned
|
||||
"""
|
||||
user_alpha1 = self.create_user(
|
||||
"alpha1", "password", "Alpha", email="alpha1@superset.org"
|
||||
@@ -455,7 +465,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_create_dashboard(self):
|
||||
"""
|
||||
Dashboard API: Test create dashboard
|
||||
Dashboard API: Test create dashboard
|
||||
"""
|
||||
admin_id = self.get_user("admin").id
|
||||
dashboard_data = {
|
||||
@@ -469,7 +479,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
}
|
||||
self.login(username="admin")
|
||||
uri = "api/v1/dashboard/"
|
||||
rv = self.client.post(uri, json=dashboard_data)
|
||||
rv = self.post_assert_metric(uri, dashboard_data, "post")
|
||||
self.assertEqual(rv.status_code, 201)
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
model = db.session.query(Dashboard).get(data.get("id"))
|
||||
@@ -478,7 +488,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_create_simple_dashboard(self):
|
||||
"""
|
||||
Dashboard API: Test create simple dashboard
|
||||
Dashboard API: Test create simple dashboard
|
||||
"""
|
||||
dashboard_data = {"dashboard_title": "title1"}
|
||||
self.login(username="admin")
|
||||
@@ -492,7 +502,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_create_dashboard_empty(self):
|
||||
"""
|
||||
Dashboard API: Test create empty
|
||||
Dashboard API: Test create empty
|
||||
"""
|
||||
dashboard_data = {}
|
||||
self.login(username="admin")
|
||||
@@ -516,12 +526,12 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_create_dashboard_validate_title(self):
|
||||
"""
|
||||
Dashboard API: Test create dashboard validate title
|
||||
Dashboard API: Test create dashboard validate title
|
||||
"""
|
||||
dashboard_data = {"dashboard_title": "a" * 600}
|
||||
self.login(username="admin")
|
||||
uri = "api/v1/dashboard/"
|
||||
rv = self.client.post(uri, json=dashboard_data)
|
||||
rv = self.post_assert_metric(uri, dashboard_data, "post")
|
||||
self.assertEqual(rv.status_code, 400)
|
||||
response = json.loads(rv.data.decode("utf-8"))
|
||||
expected_response = {
|
||||
@@ -531,7 +541,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_create_dashboard_validate_slug(self):
|
||||
"""
|
||||
Dashboard API: Test create validate slug
|
||||
Dashboard API: Test create validate slug
|
||||
"""
|
||||
admin_id = self.get_user("admin").id
|
||||
dashboard = self.insert_dashboard("title1", "slug1", [admin_id])
|
||||
@@ -560,7 +570,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_create_dashboard_validate_owners(self):
|
||||
"""
|
||||
Dashboard API: Test create validate owners
|
||||
Dashboard API: Test create validate owners
|
||||
"""
|
||||
dashboard_data = {"dashboard_title": "title1", "owners": [1000]}
|
||||
self.login(username="admin")
|
||||
@@ -573,7 +583,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_create_dashboard_validate_json(self):
|
||||
"""
|
||||
Dashboard API: Test create validate json
|
||||
Dashboard API: Test create validate json
|
||||
"""
|
||||
dashboard_data = {"dashboard_title": "title1", "position_json": '{"A:"a"}'}
|
||||
self.login(username="admin")
|
||||
@@ -598,13 +608,13 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_update_dashboard(self):
|
||||
"""
|
||||
Dashboard API: Test update
|
||||
Dashboard API: Test update
|
||||
"""
|
||||
admin = self.get_user("admin")
|
||||
dashboard_id = self.insert_dashboard("title1", "slug1", [admin.id]).id
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dashboard/{dashboard_id}"
|
||||
rv = self.client.put(uri, json=self.dashboard_data)
|
||||
rv = self.put_assert_metric(uri, self.dashboard_data, "put")
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
model = db.session.query(Dashboard).get(dashboard_id)
|
||||
self.assertEqual(model.dashboard_title, self.dashboard_data["dashboard_title"])
|
||||
@@ -620,7 +630,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_update_dashboard_chart_owners(self):
|
||||
"""
|
||||
Dashboard API: Test update chart owners
|
||||
Dashboard API: Test update chart owners
|
||||
"""
|
||||
user_alpha1 = self.create_user(
|
||||
"alpha1", "password", "Alpha", email="alpha1@superset.org"
|
||||
@@ -663,7 +673,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_update_partial_dashboard(self):
|
||||
"""
|
||||
Dashboard API: Test update partial
|
||||
Dashboard API: Test update partial
|
||||
"""
|
||||
admin_id = self.get_user("admin").id
|
||||
dashboard_id = self.insert_dashboard("title1", "slug1", [admin_id]).id
|
||||
@@ -692,7 +702,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_update_dashboard_new_owner(self):
|
||||
"""
|
||||
Dashboard API: Test update set new owner to current user
|
||||
Dashboard API: Test update set new owner to current user
|
||||
"""
|
||||
gamma_id = self.get_user("gamma").id
|
||||
admin = self.get_user("admin")
|
||||
@@ -711,7 +721,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_update_dashboard_slug_formatting(self):
|
||||
"""
|
||||
Dashboard API: Test update slug formatting
|
||||
Dashboard API: Test update slug formatting
|
||||
"""
|
||||
admin_id = self.get_user("admin").id
|
||||
dashboard_id = self.insert_dashboard("title1", "slug1", [admin_id]).id
|
||||
@@ -728,7 +738,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_update_dashboard_validate_slug(self):
|
||||
"""
|
||||
Dashboard API: Test update validate slug
|
||||
Dashboard API: Test update validate slug
|
||||
"""
|
||||
admin_id = self.get_user("admin").id
|
||||
dashboard1 = self.insert_dashboard("title1", "slug-1", [admin_id])
|
||||
@@ -763,7 +773,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_update_published(self):
|
||||
"""
|
||||
Dashboard API: Test update published patch
|
||||
Dashboard API: Test update published patch
|
||||
"""
|
||||
admin = self.get_user("admin")
|
||||
gamma = self.get_user("gamma")
|
||||
@@ -785,7 +795,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_update_dashboard_not_owned(self):
|
||||
"""
|
||||
Dashboard API: Test update dashboard not owned
|
||||
Dashboard API: Test update dashboard not owned
|
||||
"""
|
||||
user_alpha1 = self.create_user(
|
||||
"alpha1", "password", "Alpha", email="alpha1@superset.org"
|
||||
@@ -802,7 +812,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
self.login(username="alpha2", password="password")
|
||||
dashboard_data = {"dashboard_title": "title1_changed", "slug": "slug1 changed"}
|
||||
uri = f"api/v1/dashboard/{dashboard.id}"
|
||||
rv = self.client.put(uri, json=dashboard_data)
|
||||
rv = self.put_assert_metric(uri, dashboard_data, "put")
|
||||
self.assertEqual(rv.status_code, 403)
|
||||
db.session.delete(dashboard)
|
||||
db.session.delete(user_alpha1)
|
||||
@@ -811,13 +821,12 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_export(self):
|
||||
"""
|
||||
Dashboard API: Test dashboard export
|
||||
Dashboard API: Test dashboard export
|
||||
"""
|
||||
self.login(username="admin")
|
||||
argument = [1, 2]
|
||||
uri = f"api/v1/dashboard/export/?q={prison.dumps(argument)}"
|
||||
|
||||
rv = self.client.get(uri)
|
||||
rv = self.get_assert_metric(uri, "export")
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
self.assertEqual(
|
||||
rv.headers["Content-Disposition"],
|
||||
@@ -826,7 +835,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_export_not_found(self):
|
||||
"""
|
||||
Dashboard API: Test dashboard export not found
|
||||
Dashboard API: Test dashboard export not found
|
||||
"""
|
||||
self.login(username="admin")
|
||||
argument = [1000]
|
||||
@@ -836,7 +845,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
|
||||
def test_export_not_allowed(self):
|
||||
"""
|
||||
Dashboard API: Test dashboard export not allowed
|
||||
Dashboard API: Test dashboard export not allowed
|
||||
"""
|
||||
admin_id = self.get_user("admin").id
|
||||
dashboard = self.insert_dashboard("title", "slug1", [admin_id], published=False)
|
||||
|
||||
Reference in New Issue
Block a user