Remove pagination, refactoring
This commit is contained in:
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
## ToDo
|
## ToDo
|
||||||
|
|
||||||
1. Implement infinity scroll
|
1. Implement cursor pagination on backends
|
||||||
2. Implement S3 backend client
|
2. Implement infinity scroll
|
||||||
3. Implement FTP backend client
|
3. Implement S3 backend client
|
||||||
|
4. Implement FTP backend client
|
||||||
|
|
||||||
## User Flow
|
## User Flow
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from soul_diary.backend.database.models import Sense, Session
|
|||||||
from .dependencies import is_auth, sense
|
from .dependencies import is_auth, sense
|
||||||
from .schemas import (
|
from .schemas import (
|
||||||
CreateSenseRequest,
|
CreateSenseRequest,
|
||||||
Pagination,
|
|
||||||
SenseListResponse,
|
SenseListResponse,
|
||||||
SenseResponse,
|
SenseResponse,
|
||||||
UpdateSenseRequest,
|
UpdateSenseRequest,
|
||||||
@@ -16,15 +15,9 @@ from .schemas import (
|
|||||||
async def get_sense_list(
|
async def get_sense_list(
|
||||||
database: DatabaseService = fastapi.Depends(database),
|
database: DatabaseService = fastapi.Depends(database),
|
||||||
user_session: Session = fastapi.Depends(is_auth),
|
user_session: Session = fastapi.Depends(is_auth),
|
||||||
pagination: Pagination = fastapi.Depends(Pagination),
|
|
||||||
) -> SenseListResponse:
|
) -> SenseListResponse:
|
||||||
async with database.transaction() as session:
|
async with database.transaction() as session:
|
||||||
senses = await database.get_sense_list(
|
senses = await database.get_senses(session=session, user=user_session.user)
|
||||||
session=session,
|
|
||||||
user=user_session.user,
|
|
||||||
page=pagination.page,
|
|
||||||
limit=pagination.limit,
|
|
||||||
)
|
|
||||||
|
|
||||||
return SenseListResponse(data=senses)
|
return SenseListResponse(data=senses)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import AwareDatetime, BaseModel, ConfigDict, PositiveInt
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
|
||||||
class CreateSenseRequest(BaseModel):
|
class CreateSenseRequest(BaseModel):
|
||||||
@@ -20,10 +20,5 @@ class SenseResponse(BaseModel):
|
|||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
class Pagination(BaseModel):
|
|
||||||
page: PositiveInt = 1
|
|
||||||
limit: PositiveInt = 10
|
|
||||||
|
|
||||||
|
|
||||||
class SenseListResponse(BaseModel):
|
class SenseListResponse(BaseModel):
|
||||||
data: list[SenseResponse]
|
data: list[SenseResponse]
|
||||||
|
|||||||
@@ -99,18 +99,8 @@ class DatabaseService(ServiceMixin):
|
|||||||
|
|
||||||
return user_session
|
return user_session
|
||||||
|
|
||||||
async def get_sense_list(
|
async def get_senses(self, session: AsyncSession, user: User) -> list[Sense]:
|
||||||
self,
|
query = select(Sense).where(Sense.user == user).order_by(Sense.created_at.desc())
|
||||||
session: AsyncSession,
|
|
||||||
user: User,
|
|
||||||
page: int = 1,
|
|
||||||
limit: int = 10,
|
|
||||||
) -> list[Sense]:
|
|
||||||
query = (
|
|
||||||
select(Sense).where(Sense.user == user)
|
|
||||||
.order_by(Sense.created_at.desc())
|
|
||||||
.limit(limit).offset((page - 1) * limit)
|
|
||||||
)
|
|
||||||
|
|
||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
senses = result.scalars().all()
|
senses = result.scalars().all()
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ from typing import Any
|
|||||||
|
|
||||||
from Cryptodome.Cipher import AES
|
from Cryptodome.Cipher import AES
|
||||||
|
|
||||||
from soul_diary.ui.app.backend.models import SenseBackendData
|
|
||||||
from soul_diary.ui.app.local_storage import LocalStorage
|
from soul_diary.ui.app.local_storage import LocalStorage
|
||||||
from soul_diary.ui.app.models import BackendType, Emotion, Options, Sense
|
from soul_diary.ui.app.models import BackendType, Emotion, Sense
|
||||||
|
from .models import EncryptedSense, EncryptedSenseList, SenseList, Options
|
||||||
|
|
||||||
|
|
||||||
class BaseBackend:
|
class BaseBackend:
|
||||||
@@ -68,7 +68,7 @@ class BaseBackend:
|
|||||||
|
|
||||||
return data_decoded
|
return data_decoded
|
||||||
|
|
||||||
def convert_sense_data_to_sense(self, sense_data: SenseBackendData) -> Sense:
|
def convert_encrypted_sense_to_sense(self, sense_data: EncryptedSense) -> Sense:
|
||||||
return Sense(
|
return Sense(
|
||||||
id=sense_data.id,
|
id=sense_data.id,
|
||||||
created_at=sense_data.created_at,
|
created_at=sense_data.created_at,
|
||||||
@@ -110,12 +110,13 @@ class BaseBackend:
|
|||||||
def is_auth(self) -> bool:
|
def is_auth(self) -> bool:
|
||||||
return all((self._token, self._encryption_key))
|
return all((self._token, self._encryption_key))
|
||||||
|
|
||||||
async def get_sense_list(self, page: int = 1, limit: int = 10) -> list[Sense]:
|
async def get_sense_list(self) -> SenseList:
|
||||||
sense_data_list = await self.fetch_sense_list(page=page, limit=limit)
|
encrypted_sense_list = await self.fetch_sense_list()
|
||||||
return [
|
senses = [
|
||||||
self.convert_sense_data_to_sense(sense_data)
|
self.convert_encrypted_sense_to_sense(encrypted_sense)
|
||||||
for sense_data in sense_data_list
|
for encrypted_sense in encrypted_sense_list.senses
|
||||||
]
|
]
|
||||||
|
return SenseList(senses=senses)
|
||||||
|
|
||||||
async def create_sense(
|
async def create_sense(
|
||||||
self,
|
self,
|
||||||
@@ -132,13 +133,13 @@ class BaseBackend:
|
|||||||
}
|
}
|
||||||
encoded_data = self.encode(data)
|
encoded_data = self.encode(data)
|
||||||
|
|
||||||
sense_data = await self.pull_sense_data(data=encoded_data)
|
encrypted_sense = await self.pull_sense_data(data=encoded_data)
|
||||||
|
|
||||||
return self.convert_sense_data_to_sense(sense_data)
|
return self.convert_encrypted_sense_to_sense(encrypted_sense)
|
||||||
|
|
||||||
async def get_sense(self, sense_id: uuid.UUID) -> Sense:
|
async def get_sense(self, sense_id: uuid.UUID) -> Sense:
|
||||||
sense_data = await self.fetch_sense(sense_id=sense_id)
|
encrypted_sense = await self.fetch_sense(sense_id=sense_id)
|
||||||
return self.convert_sense_data_to_sense(sense_data)
|
return self.convert_encrypted_sense_to_sense(encrypted_sense)
|
||||||
|
|
||||||
async def edit_sense(
|
async def edit_sense(
|
||||||
self,
|
self,
|
||||||
@@ -156,9 +157,9 @@ class BaseBackend:
|
|||||||
}
|
}
|
||||||
encoded_data = self.encode(data)
|
encoded_data = self.encode(data)
|
||||||
|
|
||||||
sense_data = await self.pull_sense_data(data=encoded_data, sense_id=sense_id)
|
encrypted_sense = await self.pull_sense_data(data=encoded_data, sense_id=sense_id)
|
||||||
|
|
||||||
return self.convert_sense_data_to_sense(sense_data)
|
return self.convert_encrypted_sense_to_sense(encrypted_sense)
|
||||||
|
|
||||||
def get_backend_data(self) -> dict[str, Any]:
|
def get_backend_data(self) -> dict[str, Any]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@@ -175,21 +176,17 @@ class BaseBackend:
|
|||||||
async def get_options(self) -> Options:
|
async def get_options(self) -> Options:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def fetch_sense_list(
|
async def fetch_sense_list(self) -> EncryptedSenseList:
|
||||||
self,
|
|
||||||
page: int = 1,
|
|
||||||
limit: int = 10,
|
|
||||||
) -> list[SenseBackendData]:
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def fetch_sense(self, sense_id: uuid.UUID) -> SenseBackendData:
|
async def fetch_sense(self, sense_id: uuid.UUID) -> EncryptedSense:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def pull_sense_data(
|
async def pull_sense_data(
|
||||||
self,
|
self,
|
||||||
data: str,
|
data: str,
|
||||||
sense_id: uuid.UUID | None = None,
|
sense_id: uuid.UUID | None = None,
|
||||||
) -> SenseBackendData:
|
) -> EncryptedSense:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def delete_sense(self, sense_id: uuid.UUID):
|
async def delete_sense(self, sense_id: uuid.UUID):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import uuid
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from soul_diary.ui.app.models import BackendType, Options
|
from soul_diary.ui.app.models import BackendType
|
||||||
from .base import BaseBackend
|
from .base import BaseBackend
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
IncorrectCredentialsException,
|
IncorrectCredentialsException,
|
||||||
@@ -11,7 +11,7 @@ from .exceptions import (
|
|||||||
SenseNotFoundException,
|
SenseNotFoundException,
|
||||||
UserAlreadyExistsException,
|
UserAlreadyExistsException,
|
||||||
)
|
)
|
||||||
from .models import SenseBackendData
|
from .models import EncryptedSense, EncryptedSenseList, Options
|
||||||
|
|
||||||
|
|
||||||
class LocalBackend(BaseBackend):
|
class LocalBackend(BaseBackend):
|
||||||
@@ -60,48 +60,39 @@ class LocalBackend(BaseBackend):
|
|||||||
async def get_options(self) -> Options:
|
async def get_options(self) -> Options:
|
||||||
return Options(registration_enabled=True)
|
return Options(registration_enabled=True)
|
||||||
|
|
||||||
async def _fetch_sense_list(self) -> list[SenseBackendData]:
|
async def fetch_sense_list(self) -> EncryptedSenseList:
|
||||||
if not self.is_auth:
|
if not self.is_auth:
|
||||||
raise NonAuthenticatedException()
|
raise NonAuthenticatedException()
|
||||||
|
|
||||||
sense_list_key = self.SENSE_LIST_KEY_TEMPLATE.format(username=self._username)
|
sense_list_key = self.SENSE_LIST_KEY_TEMPLATE.format(username=self._username)
|
||||||
sense_list = await self._local_storage.raw_read(sense_list_key) or []
|
sense_list = await self._local_storage.raw_read(sense_list_key) or []
|
||||||
return [SenseBackendData.model_validate(sense) for sense in sense_list]
|
senses = [EncryptedSense.model_validate(sense) for sense in sense_list]
|
||||||
|
return EncryptedSenseList(senses=senses)
|
||||||
|
|
||||||
async def fetch_sense_list(
|
async def fetch_sense(self, sense_id: uuid.UUID) -> EncryptedSense:
|
||||||
self,
|
sense_list = await self.fetch_sense_list()
|
||||||
page: int = 1,
|
|
||||||
limit: int = 10,
|
|
||||||
) -> list[SenseBackendData]:
|
|
||||||
sense_list = await self._fetch_sense_list()
|
|
||||||
sense_list_filtered = sense_list[(page - 1) * limit:page * limit]
|
|
||||||
|
|
||||||
return sense_list_filtered
|
for sense in sense_list.senses:
|
||||||
|
|
||||||
async def fetch_sense(self, sense_id: uuid.UUID) -> SenseBackendData:
|
|
||||||
sense_list = await self._fetch_sense_list()
|
|
||||||
|
|
||||||
for sense in sense_list:
|
|
||||||
if sense.id == sense_id:
|
if sense.id == sense_id:
|
||||||
return sense
|
return sense
|
||||||
|
|
||||||
raise SenseNotFoundException()
|
raise SenseNotFoundException()
|
||||||
|
|
||||||
async def pull_sense_data(self, data: str, sense_id: uuid.UUID | None = None) -> SenseBackendData:
|
async def pull_sense_data(self, data: str, sense_id: uuid.UUID | None = None) -> EncryptedSense:
|
||||||
sense_list_key = self.SENSE_LIST_KEY_TEMPLATE.format(username=self._username)
|
sense_list_key = self.SENSE_LIST_KEY_TEMPLATE.format(username=self._username)
|
||||||
sense_list = await self._fetch_sense_list()
|
sense_list = await self.fetch_sense_list()
|
||||||
|
|
||||||
if sense_id is None:
|
if sense_id is None:
|
||||||
sense_ids = {sense.id for sense in sense_list}
|
sense_ids = {sense.id for sense in sense_list.senses}
|
||||||
sense_id = uuid.uuid4()
|
sense_id = uuid.uuid4()
|
||||||
while sense_id in sense_ids:
|
while sense_id in sense_ids:
|
||||||
sense_id = uuid.uuid4()
|
sense_id = uuid.uuid4()
|
||||||
sense = SenseBackendData(
|
sense = EncryptedSense(
|
||||||
id=sense_id,
|
id=sense_id,
|
||||||
data=data,
|
data=data,
|
||||||
created_at=datetime.now().astimezone(),
|
created_at=datetime.now().astimezone(),
|
||||||
)
|
)
|
||||||
sense_list.insert(0, sense)
|
sense_list.senses.insert(0, sense)
|
||||||
else:
|
else:
|
||||||
for index, sense in enumerate(sense_list):
|
for index, sense in enumerate(sense_list):
|
||||||
if sense.id == sense_id:
|
if sense.id == sense_id:
|
||||||
@@ -109,20 +100,20 @@ class LocalBackend(BaseBackend):
|
|||||||
else:
|
else:
|
||||||
raise SenseNotFoundException()
|
raise SenseNotFoundException()
|
||||||
|
|
||||||
sense = sense_list[index]
|
sense = sense_list.senses[index]
|
||||||
sense.data = data
|
sense.data = data
|
||||||
sense_list[index] = sense
|
sense_list.senses[index] = sense
|
||||||
|
|
||||||
await self._local_storage.raw_write(
|
await self._local_storage.raw_write(
|
||||||
sense_list_key,
|
sense_list_key,
|
||||||
[sense.model_dump(mode="json") for sense in sense_list],
|
[sense.model_dump(mode="json") for sense in sense_list.senses],
|
||||||
)
|
)
|
||||||
|
|
||||||
return sense
|
return sense
|
||||||
|
|
||||||
async def delete_sense(self, sense_id: uuid.UUID):
|
async def delete_sense(self, sense_id: uuid.UUID):
|
||||||
sense_list_key = self.SENSE_LIST_KEY_TEMPLATE.format(username=self._username)
|
sense_list_key = self.SENSE_LIST_KEY_TEMPLATE.format(username=self._username)
|
||||||
sense_list = await self._fetch_sense_list()
|
sense_list = await self.fetch_sense_list()
|
||||||
|
|
||||||
for index, sense in enumerate(sense_list):
|
for index, sense in enumerate(sense_list):
|
||||||
if sense.id == sense_id:
|
if sense.id == sense_id:
|
||||||
|
|||||||
@@ -3,8 +3,22 @@ from datetime import datetime
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from soul_diary.ui.app.models import Sense
|
||||||
|
|
||||||
class SenseBackendData(BaseModel):
|
|
||||||
|
class EncryptedSense(BaseModel):
|
||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
data: str
|
data: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptedSenseList(BaseModel):
|
||||||
|
senses: list[EncryptedSense]
|
||||||
|
|
||||||
|
|
||||||
|
class SenseList(BaseModel):
|
||||||
|
senses: list[Sense]
|
||||||
|
|
||||||
|
|
||||||
|
class Options(BaseModel):
|
||||||
|
registration_enabled: bool
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import httpx
|
|||||||
import yarl
|
import yarl
|
||||||
|
|
||||||
from soul_diary.ui.app.local_storage import LocalStorage
|
from soul_diary.ui.app.local_storage import LocalStorage
|
||||||
from soul_diary.ui.app.models import BackendType, Options
|
from soul_diary.ui.app.models import BackendType
|
||||||
from .base import BaseBackend
|
from .base import BaseBackend
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
IncorrectCredentialsException,
|
IncorrectCredentialsException,
|
||||||
@@ -14,7 +14,7 @@ from .exceptions import (
|
|||||||
SenseNotFoundException,
|
SenseNotFoundException,
|
||||||
UserAlreadyExistsException,
|
UserAlreadyExistsException,
|
||||||
)
|
)
|
||||||
from .models import SenseBackendData
|
from .models import EncryptedSense, EncryptedSenseList, Options
|
||||||
|
|
||||||
|
|
||||||
class SoulBackend(BaseBackend):
|
class SoulBackend(BaseBackend):
|
||||||
@@ -117,20 +117,15 @@ class SoulBackend(BaseBackend):
|
|||||||
|
|
||||||
return Options.model_validate(response)
|
return Options.model_validate(response)
|
||||||
|
|
||||||
async def fetch_sense_list(
|
async def fetch_sense_list(self) -> EncryptedSenseList:
|
||||||
self,
|
|
||||||
page: int = 1,
|
|
||||||
limit: int = 10,
|
|
||||||
) -> list[SenseBackendData]:
|
|
||||||
path = "/senses/"
|
path = "/senses/"
|
||||||
params = {"page": page, "limit": limit}
|
|
||||||
|
|
||||||
response = await self.request(method="GET", path=path, params=params)
|
response = await self.request(method="GET", path=path)
|
||||||
senses = [SenseBackendData.model_validate(sense) for sense in response["data"]]
|
senses = [EncryptedSense.model_validate(sense) for sense in response["data"]]
|
||||||
|
|
||||||
return senses
|
return EncryptedSenseList(senses=senses)
|
||||||
|
|
||||||
async def fetch_sense(self, sense_id: uuid.UUID) -> SenseBackendData:
|
async def fetch_sense(self, sense_id: uuid.UUID) -> EncryptedSense:
|
||||||
path = f"/senses/{sense_id}"
|
path = f"/senses/{sense_id}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -141,13 +136,13 @@ class SoulBackend(BaseBackend):
|
|||||||
else:
|
else:
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
return SenseBackendData.model_validate(response)
|
return EncryptedSense.model_validate(response)
|
||||||
|
|
||||||
async def pull_sense_data(
|
async def pull_sense_data(
|
||||||
self,
|
self,
|
||||||
data: str,
|
data: str,
|
||||||
sense_id: uuid.UUID | None = None,
|
sense_id: uuid.UUID | None = None,
|
||||||
) -> SenseBackendData:
|
) -> EncryptedSense:
|
||||||
path = "/senses/" if sense_id is None else f"/senses/{sense_id}"
|
path = "/senses/" if sense_id is None else f"/senses/{sense_id}"
|
||||||
request_data = {"data": data}
|
request_data = {"data": data}
|
||||||
|
|
||||||
@@ -159,7 +154,7 @@ class SoulBackend(BaseBackend):
|
|||||||
else:
|
else:
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
return SenseBackendData.model_validate(response)
|
return EncryptedSense.model_validate(response)
|
||||||
|
|
||||||
async def delete_sense(self, sense_id: uuid.UUID):
|
async def delete_sense(self, sense_id: uuid.UUID):
|
||||||
path = f"/senses/{sense_id}"
|
path = f"/senses/{sense_id}"
|
||||||
|
|||||||
@@ -26,7 +26,3 @@ class Sense(BaseModel):
|
|||||||
body: constr(min_length=1, strip_whitespace=True)
|
body: constr(min_length=1, strip_whitespace=True)
|
||||||
desires: constr(min_length=1, strip_whitespace=True)
|
desires: constr(min_length=1, strip_whitespace=True)
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
class Options(BaseModel):
|
|
||||||
registration_enabled: bool
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class DesiresPage(BasePage):
|
|||||||
body=body,
|
body=body,
|
||||||
).apply()
|
).apply()
|
||||||
|
|
||||||
@callback_error_handle
|
# @callback_error_handle
|
||||||
async def callback_add_sense(self, event: flet.ControlEvent, desires_field: flet.TextField):
|
async def callback_add_sense(self, event: flet.ControlEvent, desires_field: flet.TextField):
|
||||||
if self.desires is None or not self.desires.strip():
|
if self.desires is None or not self.desires.strip():
|
||||||
desires_field.error_text = "Коротко опиши свои желания"
|
desires_field.error_text = "Коротко опиши свои желания"
|
||||||
|
|||||||
@@ -65,12 +65,13 @@ class SenseListPage(BasePage):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def did_mount_async(self):
|
async def did_mount_async(self):
|
||||||
|
backend_client = await get_backend_client(self.local_storage)
|
||||||
|
sense_list = await backend_client.get_sense_list()
|
||||||
|
self.senses = sense_list.senses
|
||||||
await self.render_cards()
|
await self.render_cards()
|
||||||
|
|
||||||
async def render_cards(self):
|
async def render_cards(self):
|
||||||
function = self.render_extend_card if self.extend else self.render_compact_card
|
function = self.render_extend_card if self.extend else self.render_compact_card
|
||||||
backend_client = await get_backend_client(self.local_storage)
|
|
||||||
self.senses = await backend_client.get_sense_list()
|
|
||||||
self.senses_cards.controls = [await function(sense) for sense in self.senses]
|
self.senses_cards.controls = [await function(sense) for sense in self.senses]
|
||||||
await self.update_async()
|
await self.update_async()
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ class SenseListPage(BasePage):
|
|||||||
content=flet.Card(
|
content=flet.Card(
|
||||||
content=flet.Container(
|
content=flet.Container(
|
||||||
content=flet.Column(controls=[feelings, bottom_row]),
|
content=flet.Column(controls=[feelings, bottom_row]),
|
||||||
padding=10,
|
padding=15,
|
||||||
),
|
),
|
||||||
width=600,
|
width=600,
|
||||||
height=150,
|
height=150,
|
||||||
@@ -150,7 +151,7 @@ class SenseListPage(BasePage):
|
|||||||
content=flet.Container(
|
content=flet.Container(
|
||||||
content=flet.Column(controls=[title, emotions, feelings_container, body_container,
|
content=flet.Column(controls=[title, emotions, feelings_container, body_container,
|
||||||
desires_container]),
|
desires_container]),
|
||||||
padding=10,
|
padding=15,
|
||||||
),
|
),
|
||||||
width=600,
|
width=600,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user