mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-03 07:55:36 +00:00
[api] Add repo last modifier and add cache for memcache ops
This commit is contained in:
@@ -15,7 +15,7 @@ from seahub.api2.authentication import TokenAuthentication
|
|||||||
from seahub.api2.throttling import UserRateThrottle
|
from seahub.api2.throttling import UserRateThrottle
|
||||||
from seahub.profile.models import Profile
|
from seahub.profile.models import Profile
|
||||||
from seahub.utils import is_org_context, is_valid_username, send_perm_audit_msg
|
from seahub.utils import is_org_context, is_valid_username, send_perm_audit_msg
|
||||||
from seahub.base.templatetags.seahub_tags import email2nickname
|
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -61,6 +61,9 @@ class SharedRepos(APIView):
|
|||||||
result['repo_name'] = repo.repo_name
|
result['repo_name'] = repo.repo_name
|
||||||
result['share_type'] = repo.share_type
|
result['share_type'] = repo.share_type
|
||||||
result['share_permission'] = repo.permission
|
result['share_permission'] = repo.permission
|
||||||
|
result['modifier_email'] = repo.last_modifier
|
||||||
|
result['modifier_name'] = email2nickname(repo.last_modifier)
|
||||||
|
result['modifier_contact_email'] = email2contact_email(repo.last_modifier)
|
||||||
|
|
||||||
if repo.share_type == 'personal':
|
if repo.share_type == 'personal':
|
||||||
result['user_name'] = email2nickname(repo.user)
|
result['user_name'] = email2nickname(repo.user)
|
||||||
|
@@ -417,6 +417,10 @@ class Repos(APIView):
|
|||||||
|
|
||||||
email = request.user.username
|
email = request.user.username
|
||||||
|
|
||||||
|
# Use dict to reduce memcache fetch cost in large for-loop.
|
||||||
|
contact_email_dict = {}
|
||||||
|
nickname_dict = {}
|
||||||
|
|
||||||
repos_json = []
|
repos_json = []
|
||||||
if filter_by['mine']:
|
if filter_by['mine']:
|
||||||
if is_org_context(request):
|
if is_org_context(request):
|
||||||
@@ -427,6 +431,14 @@ class Repos(APIView):
|
|||||||
owned_repos = seafile_api.get_owned_repo_list(email,
|
owned_repos = seafile_api.get_owned_repo_list(email,
|
||||||
ret_corrupted=True)
|
ret_corrupted=True)
|
||||||
|
|
||||||
|
# Reduce memcache fetch ops.
|
||||||
|
modifiers_set = set([x.last_modifier for x in owned_repos])
|
||||||
|
for e in modifiers_set:
|
||||||
|
if e not in contact_email_dict:
|
||||||
|
contact_email_dict[e] = email2contact_email(e)
|
||||||
|
if e not in nickname_dict:
|
||||||
|
nickname_dict[e] = email2nickname(e)
|
||||||
|
|
||||||
owned_repos.sort(lambda x, y: cmp(y.last_modify, x.last_modify))
|
owned_repos.sort(lambda x, y: cmp(y.last_modify, x.last_modify))
|
||||||
for r in owned_repos:
|
for r in owned_repos:
|
||||||
# do not return virtual repos
|
# do not return virtual repos
|
||||||
@@ -439,6 +451,9 @@ class Repos(APIView):
|
|||||||
"owner": email,
|
"owner": email,
|
||||||
"name": r.name,
|
"name": r.name,
|
||||||
"mtime": r.last_modify,
|
"mtime": r.last_modify,
|
||||||
|
"modifier_email": r.last_modifier,
|
||||||
|
"modifier_contact_email": contact_email_dict.get(r.last_modifier, ''),
|
||||||
|
"modifier_name": nickname_dict.get(r.last_modifier, ''),
|
||||||
"mtime_relative": translate_seahub_time(r.last_modify),
|
"mtime_relative": translate_seahub_time(r.last_modify),
|
||||||
"size": r.size,
|
"size": r.size,
|
||||||
"size_formatted": filesizeformat(r.size),
|
"size_formatted": filesizeformat(r.size),
|
||||||
@@ -461,6 +476,15 @@ class Repos(APIView):
|
|||||||
shared_repos = seafile_api.get_share_in_repo_list(
|
shared_repos = seafile_api.get_share_in_repo_list(
|
||||||
email, -1, -1)
|
email, -1, -1)
|
||||||
|
|
||||||
|
# Reduce memcache fetch ops.
|
||||||
|
owners_set = set([x.user for x in shared_repos])
|
||||||
|
modifiers_set = set([x.last_modifier for x in shared_repos])
|
||||||
|
for e in owners_set | modifiers_set:
|
||||||
|
if e not in contact_email_dict:
|
||||||
|
contact_email_dict[e] = email2contact_email(e)
|
||||||
|
if e not in nickname_dict:
|
||||||
|
nickname_dict[e] = email2nickname(e)
|
||||||
|
|
||||||
shared_repos.sort(lambda x, y: cmp(y.last_modify, x.last_modify))
|
shared_repos.sort(lambda x, y: cmp(y.last_modify, x.last_modify))
|
||||||
for r in shared_repos:
|
for r in shared_repos:
|
||||||
r.password_need = is_passwd_set(r.repo_id, email)
|
r.password_need = is_passwd_set(r.repo_id, email)
|
||||||
@@ -469,9 +493,12 @@ class Repos(APIView):
|
|||||||
"id": r.repo_id,
|
"id": r.repo_id,
|
||||||
"owner": r.user,
|
"owner": r.user,
|
||||||
"name": r.repo_name,
|
"name": r.repo_name,
|
||||||
"owner_nickname": email2nickname(r.user),
|
"owner_nickname": nickname_dict.get(r.user, ''),
|
||||||
"mtime": r.last_modify,
|
"mtime": r.last_modify,
|
||||||
"mtime_relative": translate_seahub_time(r.last_modify),
|
"mtime_relative": translate_seahub_time(r.last_modify),
|
||||||
|
"modifier_email": r.last_modifier,
|
||||||
|
"modifier_contact_email": contact_email_dict.get(r.last_modifier, ''),
|
||||||
|
"modifier_name": nickname_dict.get(r.last_modifier, ''),
|
||||||
"size": r.size,
|
"size": r.size,
|
||||||
"size_formatted": filesizeformat(r.size),
|
"size_formatted": filesizeformat(r.size),
|
||||||
"encrypted": r.encrypted,
|
"encrypted": r.encrypted,
|
||||||
@@ -487,6 +514,15 @@ class Repos(APIView):
|
|||||||
groups = get_groups_by_user(request)
|
groups = get_groups_by_user(request)
|
||||||
group_repos = get_group_repos(request, groups)
|
group_repos = get_group_repos(request, groups)
|
||||||
group_repos.sort(lambda x, y: cmp(y.last_modify, x.last_modify))
|
group_repos.sort(lambda x, y: cmp(y.last_modify, x.last_modify))
|
||||||
|
|
||||||
|
# Reduce memcache fetch ops.
|
||||||
|
modifiers_set = set([x.last_modifier for x in group_repos])
|
||||||
|
for e in modifiers_set:
|
||||||
|
if e not in contact_email_dict:
|
||||||
|
contact_email_dict[e] = email2contact_email(e)
|
||||||
|
if e not in nickname_dict:
|
||||||
|
nickname_dict[e] = email2nickname(e)
|
||||||
|
|
||||||
for r in group_repos:
|
for r in group_repos:
|
||||||
repo = {
|
repo = {
|
||||||
"type": "grepo",
|
"type": "grepo",
|
||||||
@@ -495,6 +531,9 @@ class Repos(APIView):
|
|||||||
"groupid": r.group.id,
|
"groupid": r.group.id,
|
||||||
"name": r.name,
|
"name": r.name,
|
||||||
"mtime": r.last_modify,
|
"mtime": r.last_modify,
|
||||||
|
"modifier_email": r.last_modifier,
|
||||||
|
"modifier_contact_email": contact_email_dict.get(r.last_modifier, ''),
|
||||||
|
"modifier_name": nickname_dict.get(r.last_modifier, ''),
|
||||||
"size": r.size,
|
"size": r.size,
|
||||||
"encrypted": r.encrypted,
|
"encrypted": r.encrypted,
|
||||||
"permission": check_permission(r.id, email),
|
"permission": check_permission(r.id, email),
|
||||||
@@ -506,6 +545,15 @@ class Repos(APIView):
|
|||||||
|
|
||||||
if filter_by['org'] and request.user.permissions.can_view_org():
|
if filter_by['org'] and request.user.permissions.can_view_org():
|
||||||
public_repos = list_inner_pub_repos(request)
|
public_repos = list_inner_pub_repos(request)
|
||||||
|
|
||||||
|
# Reduce memcache fetch ops.
|
||||||
|
modifiers_set = set([x.last_modifier for x in public_repos])
|
||||||
|
for e in modifiers_set:
|
||||||
|
if e not in contact_email_dict:
|
||||||
|
contact_email_dict[e] = email2contact_email(e)
|
||||||
|
if e not in nickname_dict:
|
||||||
|
nickname_dict[e] = email2nickname(e)
|
||||||
|
|
||||||
for r in public_repos:
|
for r in public_repos:
|
||||||
repo = {
|
repo = {
|
||||||
"type": "grepo",
|
"type": "grepo",
|
||||||
@@ -514,6 +562,9 @@ class Repos(APIView):
|
|||||||
"owner": "Organization",
|
"owner": "Organization",
|
||||||
"mtime": r.last_modified,
|
"mtime": r.last_modified,
|
||||||
"mtime_relative": translate_seahub_time(r.last_modified),
|
"mtime_relative": translate_seahub_time(r.last_modified),
|
||||||
|
"modifier_email": r.last_modifier,
|
||||||
|
"modifier_contact_email": contact_email_dict.get(r.last_modifier, ''),
|
||||||
|
"modifier_name": nickname_dict.get(r.last_modifier, ''),
|
||||||
"size": r.size,
|
"size": r.size,
|
||||||
"size_formatted": filesizeformat(r.size),
|
"size_formatted": filesizeformat(r.size),
|
||||||
"encrypted": r.encrypted,
|
"encrypted": r.encrypted,
|
||||||
@@ -806,6 +857,9 @@ class Repo(APIView):
|
|||||||
"encrypted":repo.encrypted,
|
"encrypted":repo.encrypted,
|
||||||
"root":root_id,
|
"root":root_id,
|
||||||
"permission": check_permission(repo.id, username),
|
"permission": check_permission(repo.id, username),
|
||||||
|
"modifier_email": repo.last_modifier,
|
||||||
|
"modifier_contact_email": email2contact_email(repo.last_modifier),
|
||||||
|
"modifier_name": email2nickname(repo.last_modifier),
|
||||||
}
|
}
|
||||||
if repo.encrypted:
|
if repo.encrypted:
|
||||||
repo_json["enc_version"] = repo.enc_version
|
repo_json["enc_version"] = repo.enc_version
|
||||||
@@ -3798,6 +3852,9 @@ class GroupRepos(APIView):
|
|||||||
"owner": username,
|
"owner": username,
|
||||||
"owner_nickname": email2nickname(username),
|
"owner_nickname": email2nickname(username),
|
||||||
"share_from_me": True,
|
"share_from_me": True,
|
||||||
|
"modifier_email": repo.last_modifier,
|
||||||
|
"modifier_contact_email": email2contact_email(repo.last_modifier),
|
||||||
|
"modifier_name": email2nickname(repo.last_modifier),
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response(group_repo, status=200)
|
return Response(group_repo, status=200)
|
||||||
@@ -3819,6 +3876,17 @@ class GroupRepos(APIView):
|
|||||||
repos.sort(lambda x, y: cmp(y.last_modified, x.last_modified))
|
repos.sort(lambda x, y: cmp(y.last_modified, x.last_modified))
|
||||||
group.is_staff = is_group_staff(group, request.user)
|
group.is_staff = is_group_staff(group, request.user)
|
||||||
|
|
||||||
|
# Use dict to reduce memcache fetch cost in large for-loop.
|
||||||
|
contact_email_dict = {}
|
||||||
|
nickname_dict = {}
|
||||||
|
owner_set = set([x.user for x in repos])
|
||||||
|
modifiers_set = set([x.modifier for x in repos])
|
||||||
|
for e in owner_set | modifiers_set:
|
||||||
|
if e not in contact_email_dict:
|
||||||
|
contact_email_dict[e] = email2contact_email(e)
|
||||||
|
if e not in nickname_dict:
|
||||||
|
nickname_dict[e] = email2nickname(e)
|
||||||
|
|
||||||
repos_json = []
|
repos_json = []
|
||||||
for r in repos:
|
for r in repos:
|
||||||
repo = {
|
repo = {
|
||||||
@@ -3832,8 +3900,11 @@ class GroupRepos(APIView):
|
|||||||
"encrypted": r.encrypted,
|
"encrypted": r.encrypted,
|
||||||
"permission": r.permission,
|
"permission": r.permission,
|
||||||
"owner": r.user,
|
"owner": r.user,
|
||||||
"owner_nickname": email2nickname(r.user),
|
"owner_nickname": nickname_dict.get(r.user, ''),
|
||||||
"share_from_me": True if username == r.user else False,
|
"share_from_me": True if username == r.user else False,
|
||||||
|
"modifier_email": r.last_modifier,
|
||||||
|
"modifier_contact_email": contact_email_dict.get(r.last_modifier, ''),
|
||||||
|
"modifier_name": nickname_dict.get(r.last_modifier, ''),
|
||||||
}
|
}
|
||||||
repos_json.append(repo)
|
repos_json.append(repo)
|
||||||
|
|
||||||
|
@@ -58,6 +58,9 @@ class SharedReposTest(BaseTestCase):
|
|||||||
assert json_resp[0]['user_email'] == self.admin_name
|
assert json_resp[0]['user_email'] == self.admin_name
|
||||||
assert json_resp[0]['user_name'] == nickname
|
assert json_resp[0]['user_name'] == nickname
|
||||||
assert json_resp[0]['contact_email'] == contact_email
|
assert json_resp[0]['contact_email'] == contact_email
|
||||||
|
assert len(json_resp[0]['modifier_email']) > 0
|
||||||
|
assert len(json_resp[0]['modifier_name']) > 0
|
||||||
|
assert len(json_resp[0]['modifier_contact_email']) > 0
|
||||||
|
|
||||||
def test_can_get_when_share_to_group(self):
|
def test_can_get_when_share_to_group(self):
|
||||||
self.share_repo_to_group()
|
self.share_repo_to_group()
|
||||||
|
@@ -58,6 +58,10 @@ class GroupRepoTest(ApiTestBase):
|
|||||||
assert resp_repo['mtime'] > 0
|
assert resp_repo['mtime'] > 0
|
||||||
assert resp_repo['permission'] in ('r', 'rw')
|
assert resp_repo['permission'] in ('r', 'rw')
|
||||||
assert '</time>' in resp_repo['mtime_relative']
|
assert '</time>' in resp_repo['mtime_relative']
|
||||||
|
assert len(resp_repo['owner_nickname']) > 0
|
||||||
|
assert len(resp_repo['modifier_email']) > 0
|
||||||
|
assert len(resp_repo['modifier_contact_email']) > 0
|
||||||
|
assert len(resp_repo['modifier_name']) > 0
|
||||||
|
|
||||||
def test_order_by_mtime(self):
|
def test_order_by_mtime(self):
|
||||||
with self.get_tmp_group() as group:
|
with self.get_tmp_group() as group:
|
||||||
|
@@ -147,7 +147,10 @@ class RepoGroupFolderPermTest(BaseTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp = self.client.post(url, data)
|
resp = self.client.post(url, data)
|
||||||
self.assertEqual(409, resp.status_code)
|
json_resp = json.loads(resp.content)
|
||||||
|
assert len(json_resp['failed']) == 1
|
||||||
|
assert len(json_resp['success']) == 0
|
||||||
|
assert json_resp['failed'][0]['group_id'] == self.group_id
|
||||||
|
|
||||||
def test_can_delete_folder_perm(self):
|
def test_can_delete_folder_perm(self):
|
||||||
|
|
||||||
|
@@ -65,13 +65,21 @@ class RepoOwnerTest(BaseTestCase):
|
|||||||
'/', tmp_user) == 'rw'
|
'/', tmp_user) == 'rw'
|
||||||
|
|
||||||
def test_not_reshare_to_user_after_transfer_repo(self):
|
def test_not_reshare_to_user_after_transfer_repo(self):
|
||||||
# Remove share if repo already shared to new owner
|
|
||||||
|
# remove all share
|
||||||
|
shared_repos = seafile_api.get_share_in_repo_list(self.admin.username, -1, -1)
|
||||||
|
for repo in shared_repos:
|
||||||
|
seafile_api.remove_share(repo.repo_id, self.admin.username,
|
||||||
|
self.user.username)
|
||||||
|
|
||||||
|
seafile_api.remove_share(repo.repo_id, self.user.username,
|
||||||
|
self.admin.username)
|
||||||
|
|
||||||
# share user's repo to admin with 'rw' permission
|
# share user's repo to admin with 'rw' permission
|
||||||
seafile_api.share_repo(self.user_repo_id, self.user.username,
|
seafile_api.share_repo(self.user_repo_id, self.user.username,
|
||||||
self.admin.username, 'rw')
|
self.admin.username, 'rw')
|
||||||
|
|
||||||
# repo in admin's be shared repo list
|
# assert repo in admin's be shared repo list
|
||||||
shared_repos = seafile_api.get_share_in_repo_list(self.admin.username, -1, -1)
|
shared_repos = seafile_api.get_share_in_repo_list(self.admin.username, -1, -1)
|
||||||
assert shared_repos[0].repo_name == self.repo.repo_name
|
assert shared_repos[0].repo_name == self.repo.repo_name
|
||||||
|
|
||||||
@@ -83,7 +91,7 @@ class RepoOwnerTest(BaseTestCase):
|
|||||||
resp = self.client.put(url, data, 'application/x-www-form-urlencoded')
|
resp = self.client.put(url, data, 'application/x-www-form-urlencoded')
|
||||||
self.assertEqual(200, resp.status_code)
|
self.assertEqual(200, resp.status_code)
|
||||||
|
|
||||||
# repo NOT in admin's be shared repo list
|
# assert repo NOT in admin's be shared repo list
|
||||||
shared_repos = seafile_api.get_share_in_repo_list(self.admin.username, -1, -1)
|
shared_repos = seafile_api.get_share_in_repo_list(self.admin.username, -1, -1)
|
||||||
assert len(shared_repos) == 0
|
assert len(shared_repos) == 0
|
||||||
|
|
||||||
|
@@ -146,7 +146,10 @@ class RepoUserFolderPermTest(BaseTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp = self.client.post(url, data)
|
resp = self.client.post(url, data)
|
||||||
self.assertEqual(409, resp.status_code)
|
json_resp = json.loads(resp.content)
|
||||||
|
assert len(json_resp['failed']) == 1
|
||||||
|
assert len(json_resp['success']) == 0
|
||||||
|
assert json_resp['failed'][0]['user_email'] == self.admin_email
|
||||||
|
|
||||||
def test_can_delete_folder_perm(self):
|
def test_can_delete_folder_perm(self):
|
||||||
|
|
||||||
|
@@ -2,10 +2,14 @@
|
|||||||
"""
|
"""
|
||||||
Test repos api.
|
Test repos api.
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
|
import time
|
||||||
import pytest
|
import pytest
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
from seaserv import seafile_api
|
from seaserv import seafile_api
|
||||||
|
|
||||||
from tests.api.apitestbase import ApiTestBase
|
from tests.api.apitestbase import ApiTestBase
|
||||||
from tests.api.urls import (
|
from tests.api.urls import (
|
||||||
REPOS_URL, DEFAULT_REPO_URL, GET_REPO_TOKENS_URL
|
REPOS_URL, DEFAULT_REPO_URL, GET_REPO_TOKENS_URL
|
||||||
@@ -13,6 +17,8 @@ from tests.api.urls import (
|
|||||||
from tests.common.utils import apiurl, urljoin, randstring
|
from tests.common.utils import apiurl, urljoin, randstring
|
||||||
from tests.common.common import SEAFILE_BASE_URL
|
from tests.common.common import SEAFILE_BASE_URL
|
||||||
|
|
||||||
|
from seahub.test_utils import BaseTestCase
|
||||||
|
|
||||||
# TODO: all tests should be run on an encrypted repo
|
# TODO: all tests should be run on an encrypted repo
|
||||||
class ReposApiTest(ApiTestBase):
|
class ReposApiTest(ApiTestBase):
|
||||||
def test_get_default_repo(self):
|
def test_get_default_repo(self):
|
||||||
@@ -39,6 +45,9 @@ class ReposApiTest(ApiTestBase):
|
|||||||
self.assertIsNotNone(repo['name'])
|
self.assertIsNotNone(repo['name'])
|
||||||
self.assertIsNotNone(repo['type'])
|
self.assertIsNotNone(repo['type'])
|
||||||
self.assertIsNotNone(repo['head_commit_id'])
|
self.assertIsNotNone(repo['head_commit_id'])
|
||||||
|
assert len(repo['modifier_email']) > 0
|
||||||
|
assert len(repo['modifier_name']) > 0
|
||||||
|
assert len(repo['modifier_contact_email']) > 0
|
||||||
|
|
||||||
def test_get_repo_info(self):
|
def test_get_repo_info(self):
|
||||||
with self.get_tmp_repo() as repo:
|
with self.get_tmp_repo() as repo:
|
||||||
@@ -52,7 +61,9 @@ class ReposApiTest(ApiTestBase):
|
|||||||
self.assertIsNotNone(rinfo['root'])
|
self.assertIsNotNone(rinfo['root'])
|
||||||
self.assertIsNotNone(rinfo['desc'])
|
self.assertIsNotNone(rinfo['desc'])
|
||||||
self.assertIsNotNone(rinfo['type'])
|
self.assertIsNotNone(rinfo['type'])
|
||||||
# elf.assertIsNotNone(rinfo['password_need']) # allow null here
|
assert len(rinfo['modifier_email']) > 0
|
||||||
|
assert len(rinfo['modifier_name']) > 0
|
||||||
|
assert len(rinfo['modifier_contact_email']) > 0
|
||||||
|
|
||||||
def test_get_repo_history(self):
|
def test_get_repo_history(self):
|
||||||
with self.get_tmp_repo() as repo:
|
with self.get_tmp_repo() as repo:
|
||||||
@@ -176,3 +187,35 @@ class ReposApiTest(ApiTestBase):
|
|||||||
|
|
||||||
# do some file operation
|
# do some file operation
|
||||||
self.create_file(repo['repo_id'])
|
self.create_file(repo['repo_id'])
|
||||||
|
|
||||||
|
|
||||||
|
# Uncomment following to test api performance.
|
||||||
|
# class ReposApiTest2(BaseTestCase):
|
||||||
|
# def setUp(self):
|
||||||
|
# self.num_repos = 500
|
||||||
|
# self.tmp_user = self.create_user()
|
||||||
|
# self.login_as(self.tmp_user)
|
||||||
|
|
||||||
|
# self.repo_ids = []
|
||||||
|
# for i in range(self.num_repos):
|
||||||
|
# r = self.create_repo(name='test-repo%d' % i, desc='',
|
||||||
|
# username=self.tmp_user.username, passwd=None)
|
||||||
|
# self.repo_ids.append(r)
|
||||||
|
# time.sleep(0.01)
|
||||||
|
|
||||||
|
# assert len(self.repo_ids) == self.num_repos
|
||||||
|
|
||||||
|
# def tearDown(self):
|
||||||
|
# self.remove_user(self.tmp_user.username)
|
||||||
|
# for e in self.repo_ids:
|
||||||
|
# self.remove_repo(e)
|
||||||
|
|
||||||
|
# def test_list_repos(self):
|
||||||
|
# time.sleep(1)
|
||||||
|
# print '--------> start list repos...'
|
||||||
|
|
||||||
|
# resp = self.client.get(reverse('api2-repos') + '?type=mine')
|
||||||
|
# json_resp = json.loads(resp.content)
|
||||||
|
# assert len(json_resp) == self.num_repos
|
||||||
|
|
||||||
|
# print '--------> end list repos.'
|
||||||
|
Reference in New Issue
Block a user