mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +00:00
Merge pull request #51902 from home-assistant/rc
This commit is contained in:
commit
f2bc69a653
@ -3,7 +3,7 @@
|
|||||||
"name": "Daikin AC",
|
"name": "Daikin AC",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/daikin",
|
"documentation": "https://www.home-assistant.io/integrations/daikin",
|
||||||
"requirements": ["pydaikin==2.4.1"],
|
"requirements": ["pydaikin==2.4.2"],
|
||||||
"codeowners": ["@fredrike"],
|
"codeowners": ["@fredrike"],
|
||||||
"zeroconf": ["_dkapi._tcp.local."],
|
"zeroconf": ["_dkapi._tcp.local."],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from garminconnect_aio import (
|
from garminconnect_ha import (
|
||||||
Garmin,
|
Garmin,
|
||||||
GarminConnectAuthenticationError,
|
GarminConnectAuthenticationError,
|
||||||
GarminConnectConnectionError,
|
GarminConnectConnectionError,
|
||||||
@ -13,7 +13,6 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN
|
from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN
|
||||||
@ -26,14 +25,13 @@ PLATFORMS = ["sensor"]
|
|||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
"""Set up Garmin Connect from a config entry."""
|
"""Set up Garmin Connect from a config entry."""
|
||||||
|
|
||||||
websession = async_get_clientsession(hass)
|
|
||||||
username: str = entry.data[CONF_USERNAME]
|
username: str = entry.data[CONF_USERNAME]
|
||||||
password: str = entry.data[CONF_PASSWORD]
|
password: str = entry.data[CONF_PASSWORD]
|
||||||
|
|
||||||
garmin_client = Garmin(websession, username, password)
|
api = Garmin(username, password)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await garmin_client.login()
|
await hass.async_add_executor_job(api.login)
|
||||||
except (
|
except (
|
||||||
GarminConnectAuthenticationError,
|
GarminConnectAuthenticationError,
|
||||||
GarminConnectTooManyRequestsError,
|
GarminConnectTooManyRequestsError,
|
||||||
@ -49,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||||||
_LOGGER.exception("Unknown error occurred during Garmin Connect login request")
|
_LOGGER.exception("Unknown error occurred during Garmin Connect login request")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
garmin_data = GarminConnectData(hass, garmin_client)
|
garmin_data = GarminConnectData(hass, api)
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
hass.data[DOMAIN][entry.entry_id] = garmin_data
|
hass.data[DOMAIN][entry.entry_id] = garmin_data
|
||||||
|
|
||||||
@ -81,14 +79,20 @@ class GarminConnectData:
|
|||||||
today = date.today()
|
today = date.today()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
summary = await self.client.get_user_summary(today.isoformat())
|
summary = await self.hass.async_add_executor_job(
|
||||||
body = await self.client.get_body_composition(today.isoformat())
|
self.client.get_user_summary, today.isoformat()
|
||||||
|
)
|
||||||
|
body = await self.hass.async_add_executor_job(
|
||||||
|
self.client.get_body_composition, today.isoformat()
|
||||||
|
)
|
||||||
|
|
||||||
self.data = {
|
self.data = {
|
||||||
**summary,
|
**summary,
|
||||||
**body["totalAverage"],
|
**body["totalAverage"],
|
||||||
}
|
}
|
||||||
self.data["nextAlarm"] = await self.client.get_device_alarms()
|
self.data["nextAlarm"] = await self.hass.async_add_executor_job(
|
||||||
|
self.client.get_device_alarms
|
||||||
|
)
|
||||||
except (
|
except (
|
||||||
GarminConnectAuthenticationError,
|
GarminConnectAuthenticationError,
|
||||||
GarminConnectTooManyRequestsError,
|
GarminConnectTooManyRequestsError,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Config flow for Garmin Connect integration."""
|
"""Config flow for Garmin Connect integration."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from garminconnect_aio import (
|
from garminconnect_ha import (
|
||||||
Garmin,
|
Garmin,
|
||||||
GarminConnectAuthenticationError,
|
GarminConnectAuthenticationError,
|
||||||
GarminConnectConnectionError,
|
GarminConnectConnectionError,
|
||||||
@ -11,7 +11,6 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
@ -38,15 +37,14 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input is None:
|
if user_input is None:
|
||||||
return await self._show_setup_form()
|
return await self._show_setup_form()
|
||||||
|
|
||||||
websession = async_get_clientsession(self.hass)
|
|
||||||
username = user_input[CONF_USERNAME]
|
username = user_input[CONF_USERNAME]
|
||||||
password = user_input[CONF_PASSWORD]
|
password = user_input[CONF_PASSWORD]
|
||||||
|
|
||||||
garmin_client = Garmin(websession, username, password)
|
api = Garmin(username, password)
|
||||||
|
|
||||||
errors = {}
|
errors = {}
|
||||||
try:
|
try:
|
||||||
await garmin_client.login()
|
await self.hass.async_add_executor_job(api.login)
|
||||||
except GarminConnectConnectionError:
|
except GarminConnectConnectionError:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
return await self._show_setup_form(errors)
|
return await self._show_setup_form(errors)
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
"domain": "garmin_connect",
|
"domain": "garmin_connect",
|
||||||
"name": "Garmin Connect",
|
"name": "Garmin Connect",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/garmin_connect",
|
"documentation": "https://www.home-assistant.io/integrations/garmin_connect",
|
||||||
"requirements": ["garminconnect_aio==0.1.4"],
|
"requirements": ["garminconnect_ha==0.1.6"],
|
||||||
"codeowners": ["@cyberjunky"],
|
"codeowners": ["@cyberjunky"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
}
|
}
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "ialarm",
|
"domain": "ialarm",
|
||||||
"name": "Antifurto365 iAlarm",
|
"name": "Antifurto365 iAlarm",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ialarm",
|
"documentation": "https://www.home-assistant.io/integrations/ialarm",
|
||||||
"requirements": ["pyialarm==1.8.1"],
|
"requirements": ["pyialarm==1.9.0"],
|
||||||
"codeowners": ["@RyuzakiKK"],
|
"codeowners": ["@RyuzakiKK"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "local_polling"
|
"iot_class": "local_polling"
|
||||||
|
@ -5,7 +5,7 @@ from typing import Final
|
|||||||
|
|
||||||
MAJOR_VERSION: Final = 2021
|
MAJOR_VERSION: Final = 2021
|
||||||
MINOR_VERSION: Final = 6
|
MINOR_VERSION: Final = 6
|
||||||
PATCH_VERSION: Final = "4"
|
PATCH_VERSION: Final = "5"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
||||||
|
@ -63,3 +63,7 @@ enum34==1000000000.0.0
|
|||||||
typing==1000000000.0.0
|
typing==1000000000.0.0
|
||||||
uuid==1000000000.0.0
|
uuid==1000000000.0.0
|
||||||
|
|
||||||
|
# httpcore 0.13.4 breaks several integrations
|
||||||
|
# https://github.com/home-assistant/core/issues/51778
|
||||||
|
httpcore==0.13.3
|
||||||
|
|
||||||
|
@ -635,7 +635,7 @@ gTTS==2.2.2
|
|||||||
garages-amsterdam==2.1.1
|
garages-amsterdam==2.1.1
|
||||||
|
|
||||||
# homeassistant.components.garmin_connect
|
# homeassistant.components.garmin_connect
|
||||||
garminconnect_aio==0.1.4
|
garminconnect_ha==0.1.6
|
||||||
|
|
||||||
# homeassistant.components.geniushub
|
# homeassistant.components.geniushub
|
||||||
geniushub-client==0.6.30
|
geniushub-client==0.6.30
|
||||||
@ -1352,7 +1352,7 @@ pycsspeechtts==1.0.4
|
|||||||
# pycups==1.9.73
|
# pycups==1.9.73
|
||||||
|
|
||||||
# homeassistant.components.daikin
|
# homeassistant.components.daikin
|
||||||
pydaikin==2.4.1
|
pydaikin==2.4.2
|
||||||
|
|
||||||
# homeassistant.components.danfoss_air
|
# homeassistant.components.danfoss_air
|
||||||
pydanfossair==0.1.0
|
pydanfossair==0.1.0
|
||||||
@ -1464,7 +1464,7 @@ pyhomematic==0.1.72
|
|||||||
pyhomeworks==0.0.6
|
pyhomeworks==0.0.6
|
||||||
|
|
||||||
# homeassistant.components.ialarm
|
# homeassistant.components.ialarm
|
||||||
pyialarm==1.8.1
|
pyialarm==1.9.0
|
||||||
|
|
||||||
# homeassistant.components.icloud
|
# homeassistant.components.icloud
|
||||||
pyicloud==0.10.2
|
pyicloud==0.10.2
|
||||||
|
@ -341,7 +341,7 @@ gTTS==2.2.2
|
|||||||
garages-amsterdam==2.1.1
|
garages-amsterdam==2.1.1
|
||||||
|
|
||||||
# homeassistant.components.garmin_connect
|
# homeassistant.components.garmin_connect
|
||||||
garminconnect_aio==0.1.4
|
garminconnect_ha==0.1.6
|
||||||
|
|
||||||
# homeassistant.components.geo_json_events
|
# homeassistant.components.geo_json_events
|
||||||
# homeassistant.components.usgs_earthquakes_feed
|
# homeassistant.components.usgs_earthquakes_feed
|
||||||
@ -747,7 +747,7 @@ pycomfoconnect==0.4
|
|||||||
pycoolmasternet-async==0.1.2
|
pycoolmasternet-async==0.1.2
|
||||||
|
|
||||||
# homeassistant.components.daikin
|
# homeassistant.components.daikin
|
||||||
pydaikin==2.4.1
|
pydaikin==2.4.2
|
||||||
|
|
||||||
# homeassistant.components.deconz
|
# homeassistant.components.deconz
|
||||||
pydeconz==79
|
pydeconz==79
|
||||||
@ -808,7 +808,7 @@ pyhiveapi==0.4.2
|
|||||||
pyhomematic==0.1.72
|
pyhomematic==0.1.72
|
||||||
|
|
||||||
# homeassistant.components.ialarm
|
# homeassistant.components.ialarm
|
||||||
pyialarm==1.8.1
|
pyialarm==1.9.0
|
||||||
|
|
||||||
# homeassistant.components.icloud
|
# homeassistant.components.icloud
|
||||||
pyicloud==0.10.2
|
pyicloud==0.10.2
|
||||||
|
@ -83,6 +83,10 @@ enum34==1000000000.0.0
|
|||||||
typing==1000000000.0.0
|
typing==1000000000.0.0
|
||||||
uuid==1000000000.0.0
|
uuid==1000000000.0.0
|
||||||
|
|
||||||
|
# httpcore 0.13.4 breaks several integrations
|
||||||
|
# https://github.com/home-assistant/core/issues/51778
|
||||||
|
httpcore==0.13.3
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
IGNORE_PRE_COMMIT_HOOK_ID = (
|
IGNORE_PRE_COMMIT_HOOK_ID = (
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
"""Test the Garmin Connect config flow."""
|
"""Test the Garmin Connect config flow."""
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from garminconnect_aio import (
|
from garminconnect_ha import (
|
||||||
GarminConnectAuthenticationError,
|
GarminConnectAuthenticationError,
|
||||||
GarminConnectConnectionError,
|
GarminConnectConnectionError,
|
||||||
GarminConnectTooManyRequestsError,
|
GarminConnectTooManyRequestsError,
|
||||||
)
|
)
|
||||||
import pytest
|
|
||||||
|
|
||||||
from homeassistant import config_entries, data_entry_flow
|
from homeassistant import config_entries, data_entry_flow
|
||||||
from homeassistant.components.garmin_connect.const import DOMAIN
|
from homeassistant.components.garmin_connect.const import DOMAIN
|
||||||
@ -21,37 +20,23 @@ MOCK_CONF = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="mock_garmin_connect")
|
|
||||||
def mock_garmin():
|
|
||||||
"""Mock Garmin Connect."""
|
|
||||||
with patch(
|
|
||||||
"homeassistant.components.garmin_connect.config_flow.Garmin",
|
|
||||||
) as garmin:
|
|
||||||
garmin.return_value.login.return_value = MOCK_CONF[CONF_ID]
|
|
||||||
yield garmin.return_value
|
|
||||||
|
|
||||||
|
|
||||||
async def test_show_form(hass):
|
async def test_show_form(hass):
|
||||||
"""Test that the form is served with no input."""
|
"""Test that the form is served with no input."""
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["errors"] == {}
|
|
||||||
assert result["step_id"] == config_entries.SOURCE_USER
|
assert result["step_id"] == config_entries.SOURCE_USER
|
||||||
|
|
||||||
|
|
||||||
async def test_step_user(hass):
|
async def test_step_user(hass):
|
||||||
"""Test registering an integration and finishing flow works."""
|
"""Test registering an integration and finishing flow works."""
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.garmin_connect.Garmin.login",
|
|
||||||
return_value="my@email.address",
|
|
||||||
), patch(
|
|
||||||
"homeassistant.components.garmin_connect.async_setup_entry", return_value=True
|
"homeassistant.components.garmin_connect.async_setup_entry", return_value=True
|
||||||
):
|
), patch(
|
||||||
|
"homeassistant.components.garmin_connect.config_flow.Garmin",
|
||||||
|
) as garmin:
|
||||||
|
garmin.return_value.login.return_value = MOCK_CONF[CONF_ID]
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF
|
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF
|
||||||
)
|
)
|
||||||
@ -59,60 +44,69 @@ async def test_step_user(hass):
|
|||||||
assert result["data"] == MOCK_CONF
|
assert result["data"] == MOCK_CONF
|
||||||
|
|
||||||
|
|
||||||
async def test_connection_error(hass, mock_garmin_connect):
|
async def test_connection_error(hass):
|
||||||
"""Test for connection error."""
|
"""Test for connection error."""
|
||||||
mock_garmin_connect.login.side_effect = GarminConnectConnectionError("errormsg")
|
with patch(
|
||||||
result = await hass.config_entries.flow.async_init(
|
"homeassistant.components.garmin_connect.Garmin.login",
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF
|
side_effect=GarminConnectConnectionError("errormsg"),
|
||||||
)
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}, data=MOCK_CONF
|
||||||
|
)
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["errors"] == {"base": "cannot_connect"}
|
assert result["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
async def test_authentication_error(hass, mock_garmin_connect):
|
async def test_authentication_error(hass):
|
||||||
"""Test for authentication error."""
|
"""Test for authentication error."""
|
||||||
mock_garmin_connect.login.side_effect = GarminConnectAuthenticationError("errormsg")
|
with patch(
|
||||||
result = await hass.config_entries.flow.async_init(
|
"homeassistant.components.garmin_connect.Garmin.login",
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF
|
side_effect=GarminConnectAuthenticationError("errormsg"),
|
||||||
)
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}, data=MOCK_CONF
|
||||||
|
)
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["errors"] == {"base": "invalid_auth"}
|
assert result["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
async def test_toomanyrequest_error(hass, mock_garmin_connect):
|
async def test_toomanyrequest_error(hass):
|
||||||
"""Test for toomanyrequests error."""
|
"""Test for toomanyrequests error."""
|
||||||
mock_garmin_connect.login.side_effect = GarminConnectTooManyRequestsError(
|
with patch(
|
||||||
"errormsg"
|
"homeassistant.components.garmin_connect.Garmin.login",
|
||||||
)
|
side_effect=GarminConnectTooManyRequestsError("errormsg"),
|
||||||
result = await hass.config_entries.flow.async_init(
|
):
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF
|
result = await hass.config_entries.flow.async_init(
|
||||||
)
|
DOMAIN, context={"source": "user"}, data=MOCK_CONF
|
||||||
|
)
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["errors"] == {"base": "too_many_requests"}
|
assert result["errors"] == {"base": "too_many_requests"}
|
||||||
|
|
||||||
|
|
||||||
async def test_unknown_error(hass, mock_garmin_connect):
|
async def test_unknown_error(hass):
|
||||||
"""Test for unknown error."""
|
"""Test for unknown error."""
|
||||||
mock_garmin_connect.login.side_effect = Exception
|
with patch(
|
||||||
result = await hass.config_entries.flow.async_init(
|
"homeassistant.components.garmin_connect.Garmin.login",
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF
|
side_effect=Exception,
|
||||||
)
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "user"}, data=MOCK_CONF
|
||||||
|
)
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["errors"] == {"base": "unknown"}
|
assert result["errors"] == {"base": "unknown"}
|
||||||
|
|
||||||
|
|
||||||
async def test_abort_if_already_setup(hass):
|
async def test_abort_if_already_setup(hass):
|
||||||
"""Test abort if already setup."""
|
"""Test abort if already setup."""
|
||||||
MockConfigEntry(
|
|
||||||
domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID]
|
|
||||||
).add_to_hass(hass)
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.garmin_connect.config_flow.Garmin", autospec=True
|
"homeassistant.components.garmin_connect.config_flow.Garmin",
|
||||||
) as garmin:
|
):
|
||||||
garmin.return_value.login.return_value = MOCK_CONF[CONF_ID]
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID]
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": "user"}, data=MOCK_CONF
|
DOMAIN, context={"source": "user"}, data=MOCK_CONF
|
||||||
)
|
)
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user