mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-24 17:32:48 +00:00
Merge pull request #1855 from Alfresco/feature/ACS-4966_Add_path_to_categories_api
ACS-4966 Add path to Category API
This commit is contained in:
@@ -111,6 +111,11 @@ public class CategoriesImpl implements Categories
|
||||
category.setCount(categoriesCount.getOrDefault(category.getId(), 0));
|
||||
}
|
||||
|
||||
if (parameters.getInclude().contains(Nodes.PARAM_INCLUDE_PATH))
|
||||
{
|
||||
category.setPath(getCategoryPath(category));
|
||||
}
|
||||
|
||||
return category;
|
||||
}
|
||||
|
||||
@@ -128,6 +133,10 @@ public class CategoriesImpl implements Categories
|
||||
{
|
||||
category.setCount(0);
|
||||
}
|
||||
if (parameters.getInclude().contains(Nodes.PARAM_INCLUDE_PATH))
|
||||
{
|
||||
category.setPath(getCategoryPath(category));
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
@@ -136,11 +145,18 @@ public class CategoriesImpl implements Categories
|
||||
public List<Category> getCategoryChildren(final StoreRef storeRef, final String parentCategoryId, final Parameters parameters)
|
||||
{
|
||||
final NodeRef parentNodeRef = getCategoryNodeRef(storeRef, parentCategoryId);
|
||||
final List<Category> categories = nodeService.getChildAssocs(parentNodeRef).stream()
|
||||
.filter(ca -> ContentModel.ASSOC_SUBCATEGORIES.equals(ca.getTypeQName()))
|
||||
.map(ChildAssociationRef::getChildRef)
|
||||
.map(this::mapToCategory)
|
||||
.collect(Collectors.toList());
|
||||
final List<Category> categories = nodeService.getChildAssocs(parentNodeRef)
|
||||
.stream()
|
||||
.filter(ca -> ContentModel.ASSOC_SUBCATEGORIES.equals(ca.getTypeQName()))
|
||||
.map(ChildAssociationRef::getChildRef)
|
||||
.map(this::mapToCategory)
|
||||
.peek(category -> {
|
||||
if (parameters.getInclude().contains(Nodes.PARAM_INCLUDE_PATH))
|
||||
{
|
||||
category.setPath(getCategoryPath(category));
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (parameters.getInclude().contains(INCLUDE_COUNT_PARAM))
|
||||
{
|
||||
@@ -170,6 +186,11 @@ public class CategoriesImpl implements Categories
|
||||
category.setCount(categoriesCount.getOrDefault(category.getId(), 0));
|
||||
}
|
||||
|
||||
if (parameters.getInclude().contains(Nodes.PARAM_INCLUDE_PATH))
|
||||
{
|
||||
category.setPath(getCategoryPath(category));
|
||||
}
|
||||
|
||||
return category;
|
||||
}
|
||||
|
||||
@@ -200,7 +221,16 @@ public class CategoriesImpl implements Categories
|
||||
}
|
||||
final Collection<NodeRef> actualCategories = DefaultTypeConverter.INSTANCE.getCollection(NodeRef.class, currentCategories);
|
||||
|
||||
return actualCategories.stream().map(this::mapToCategory).collect(Collectors.toList());
|
||||
return actualCategories
|
||||
.stream()
|
||||
.map(this::mapToCategory)
|
||||
.peek(category -> {
|
||||
if (parameters.getInclude().contains(Nodes.PARAM_INCLUDE_PATH))
|
||||
{
|
||||
category.setPath(getCategoryPath(category));
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -230,7 +260,16 @@ public class CategoriesImpl implements Categories
|
||||
|
||||
linkNodeToCategories(contentNodeRef, categoryNodeRefs);
|
||||
|
||||
return categoryNodeRefs.stream().map(this::mapToCategory).collect(Collectors.toList());
|
||||
return categoryNodeRefs
|
||||
.stream()
|
||||
.map(this::mapToCategory)
|
||||
.peek(category -> {
|
||||
if (parameters.getInclude().contains(Nodes.PARAM_INCLUDE_PATH))
|
||||
{
|
||||
category.setPath(getCategoryPath(category));
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -475,4 +514,16 @@ public class CategoriesImpl implements Categories
|
||||
.stream()
|
||||
.collect(Collectors.toMap(pair -> pair.getFirst().toString().replace(idPrefix, StringUtils.EMPTY), Pair::getSecond));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path for a given category in human-readable form.
|
||||
*
|
||||
* @param category Category to provide path for.
|
||||
* @return Path for a category in human-readable form.
|
||||
*/
|
||||
private String getCategoryPath(final Category category)
|
||||
{
|
||||
final NodeRef categoryNodeRef = nodes.getNode(category.getId()).getNodeRef();
|
||||
return nodeService.getPath(categoryNodeRef).toDisplayPath(nodeService, permissionService);
|
||||
}
|
||||
}
|
||||
|
@@ -35,6 +35,7 @@ public class Category
|
||||
private String parentId;
|
||||
private boolean hasChildren;
|
||||
private Integer count;
|
||||
private String path;
|
||||
|
||||
public String getId()
|
||||
{
|
||||
@@ -91,6 +92,14 @@ public class Category
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
@@ -100,19 +109,20 @@ public class Category
|
||||
return false;
|
||||
Category category = (Category) o;
|
||||
return hasChildren == category.hasChildren && Objects.equals(id, category.id) && Objects.equals(name, category.name) && Objects.equals(parentId, category.parentId)
|
||||
&& Objects.equals(count, category.count);
|
||||
&& Objects.equals(count, category.count) && Objects.equals(path, category.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(id, name, parentId, hasChildren, count);
|
||||
return Objects.hash(id, name, parentId, hasChildren, count, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "Category{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", parentId='" + parentId + '\'' + ", hasChildren=" + hasChildren + ", count=" + count + '}';
|
||||
return "Category{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", parentId='" + parentId + '\'' + ", hasChildren=" + hasChildren
|
||||
+ ", count=" + count + ", path=" + path + '}';
|
||||
}
|
||||
|
||||
public static Builder builder()
|
||||
@@ -127,6 +137,7 @@ public class Category
|
||||
private String parentId;
|
||||
private boolean hasChildren;
|
||||
private Integer count;
|
||||
private String path;
|
||||
|
||||
public Builder id(String id)
|
||||
{
|
||||
@@ -158,6 +169,12 @@ public class Category
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder path(String path)
|
||||
{
|
||||
this.path = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Category create()
|
||||
{
|
||||
final Category category = new Category();
|
||||
@@ -166,6 +183,7 @@ public class Category
|
||||
category.setParentId(parentId);
|
||||
category.setHasChildren(hasChildren);
|
||||
category.setCount(count);
|
||||
category.setPath(path);
|
||||
return category;
|
||||
}
|
||||
}
|
||||
|
@@ -60,6 +60,7 @@ import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.alfresco.model.ContentModel;
|
||||
import org.alfresco.repo.transfer.PathHelper;
|
||||
import org.alfresco.rest.api.Nodes;
|
||||
import org.alfresco.rest.api.model.Category;
|
||||
import org.alfresco.rest.api.model.Node;
|
||||
@@ -71,6 +72,7 @@ import org.alfresco.rest.framework.resource.parameters.Parameters;
|
||||
import org.alfresco.service.cmr.repository.ChildAssociationRef;
|
||||
import org.alfresco.service.cmr.repository.NodeRef;
|
||||
import org.alfresco.service.cmr.repository.NodeService;
|
||||
import org.alfresco.service.cmr.repository.Path;
|
||||
import org.alfresco.service.cmr.repository.StoreRef;
|
||||
import org.alfresco.service.cmr.search.CategoryService;
|
||||
import org.alfresco.service.cmr.security.AccessStatus;
|
||||
@@ -100,6 +102,9 @@ public class CategoriesImplTest
|
||||
private static final Category CATEGORY = createDefaultCategory();
|
||||
private static final String CONTENT_NODE_ID = "content-node-id";
|
||||
private static final NodeRef CONTENT_NODE_REF = createNodeRefWithId(CONTENT_NODE_ID);
|
||||
private static final String MOCK_ROOT_LEVEL = "/{mockRootLevel}";
|
||||
private static final String MOCK_CHILD_LEVEL = "/{mockChild}";
|
||||
private static final String MOCK_CATEGORY_PATH = "//" + MOCK_ROOT_LEVEL + "//" + MOCK_CHILD_LEVEL;
|
||||
|
||||
@Mock
|
||||
private Nodes nodesMock;
|
||||
@@ -252,6 +257,27 @@ public class CategoriesImplTest
|
||||
.isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCategoryById_includePath()
|
||||
{
|
||||
final QName categoryQName = createCmQNameOf(CATEGORY_NAME);
|
||||
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
|
||||
final ChildAssociationRef parentAssociation = createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, categoryQName);
|
||||
given(nodesMock.getNode(any())).willReturn(createNode());
|
||||
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
|
||||
given(parametersMock.getInclude()).willReturn(List.of(Nodes.PARAM_INCLUDE_PATH));
|
||||
given(nodeServiceMock.getPath(any())).willReturn(mockCategoryPath());
|
||||
|
||||
// when
|
||||
final Category actualCategory = objectUnderTest.getCategoryById(CATEGORY_ID, parametersMock);
|
||||
|
||||
assertThat(actualCategory)
|
||||
.isNotNull()
|
||||
.extracting(Category::getPath)
|
||||
.isNotNull()
|
||||
.isEqualTo(MOCK_CATEGORY_PATH);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCategoryById_notACategory()
|
||||
{
|
||||
@@ -479,6 +505,36 @@ public class CategoriesImplTest
|
||||
.isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateCategory_includePath()
|
||||
{
|
||||
final QName categoryQName = createCmQNameOf(CATEGORY_NAME);
|
||||
final NodeRef categoryNodeRef = createNodeRefWithId(CATEGORY_ID);
|
||||
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
|
||||
final ChildAssociationRef parentAssociation = createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, categoryQName);
|
||||
given(nodesMock.validateNode(PARENT_ID)).willReturn(parentCategoryNodeRef);
|
||||
given(categoryServiceMock.createCategory(parentCategoryNodeRef, CATEGORY_NAME)).willReturn(categoryNodeRef);
|
||||
given(nodesMock.getNode(any())).willReturn(createNode());
|
||||
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
|
||||
given(parametersMock.getInclude()).willReturn(List.of(Nodes.PARAM_INCLUDE_PATH));
|
||||
given(nodeServiceMock.getPath(any())).willReturn(mockCategoryPath());
|
||||
final List<Category> categoryModels = new ArrayList<>(prepareCategories());
|
||||
|
||||
// when
|
||||
final List<Category> actualCreatedCategories = objectUnderTest.createSubcategories(PARENT_ID, categoryModels, parametersMock);
|
||||
|
||||
then(categoryServiceMock).should().createCategory(any(), any());
|
||||
then(categoryServiceMock).shouldHaveNoMoreInteractions();
|
||||
|
||||
assertThat(actualCreatedCategories)
|
||||
.isNotNull()
|
||||
.hasSize(1)
|
||||
.element(0)
|
||||
.extracting(Category::getPath)
|
||||
.isNotNull()
|
||||
.isEqualTo(MOCK_CATEGORY_PATH);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateCategories_noPermissions()
|
||||
{
|
||||
@@ -628,6 +684,30 @@ public class CategoriesImplTest
|
||||
.isEqualTo(List.of(0, 2, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCategoryChildren_includePath()
|
||||
{
|
||||
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
|
||||
given(nodesMock.validateNode(PARENT_ID)).willReturn(parentCategoryNodeRef);
|
||||
given(nodesMock.isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false)).willReturn(true);
|
||||
final int childrenCount = 3;
|
||||
final List<ChildAssociationRef> childAssociationRefMocks = prepareChildAssocMocks(childrenCount, parentCategoryNodeRef);
|
||||
given(nodeServiceMock.getChildAssocs(parentCategoryNodeRef)).willReturn(childAssociationRefMocks);
|
||||
childAssociationRefMocks.forEach(this::prepareCategoryNodeMocks);
|
||||
given(parametersMock.getInclude()).willReturn(List.of(Nodes.PARAM_INCLUDE_PATH));
|
||||
given(nodeServiceMock.getPath(any())).willReturn(mockCategoryPath());
|
||||
|
||||
// when
|
||||
final List<Category> actualCategoryChildren = objectUnderTest.getCategoryChildren(PARENT_ID, parametersMock);
|
||||
|
||||
assertThat(actualCategoryChildren)
|
||||
.isNotNull()
|
||||
.hasSize(3)
|
||||
.extracting(Category::getPath)
|
||||
.isNotNull()
|
||||
.isEqualTo(List.of(MOCK_CATEGORY_PATH, MOCK_CATEGORY_PATH, MOCK_CATEGORY_PATH));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCategoryChildren_noChildren()
|
||||
{
|
||||
@@ -751,6 +831,32 @@ public class CategoriesImplTest
|
||||
.isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateCategoryById_includePath()
|
||||
{
|
||||
final String categoryNewName = "categoryNewName";
|
||||
final Category fixedCategory = createCategoryOnlyWithName(categoryNewName);
|
||||
// simulate path provided by client to check if it will be ignored
|
||||
fixedCategory.setPath("/test/TestCat");
|
||||
final QName categoryQName = createCmQNameOf(CATEGORY_NAME);
|
||||
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
|
||||
final ChildAssociationRef parentAssociation = createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, categoryQName);
|
||||
given(nodesMock.getNode(any())).willReturn(createNode(categoryNewName));
|
||||
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
|
||||
given(nodeServiceMock.moveNode(any(), any(), any(), any())).willReturn(createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, createCmQNameOf(categoryNewName)));
|
||||
given(parametersMock.getInclude()).willReturn(List.of(Nodes.PARAM_INCLUDE_PATH));
|
||||
given(nodeServiceMock.getPath(any())).willReturn(mockCategoryPath());
|
||||
|
||||
// when
|
||||
final Category actualCategory = objectUnderTest.updateCategoryById(CATEGORY_ID, fixedCategory, parametersMock);
|
||||
|
||||
assertThat(actualCategory)
|
||||
.isNotNull()
|
||||
.extracting(Category::getPath)
|
||||
.isNotNull()
|
||||
.isEqualTo(MOCK_CATEGORY_PATH);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateCategoryById_noPermission()
|
||||
{
|
||||
@@ -918,6 +1024,7 @@ public class CategoriesImplTest
|
||||
then(nodeServiceMock).should().getParentAssocs(categoryParentNodeRef);
|
||||
then(nodeServiceMock).shouldHaveNoMoreInteractions();
|
||||
final List<Category> expectedLinkedCategories = List.of(CATEGORY);
|
||||
expectedLinkedCategories.get(0).setPath(null);
|
||||
assertThat(actualLinkedCategories)
|
||||
.isNotNull().usingRecursiveComparison()
|
||||
.isEqualTo(expectedLinkedCategories);
|
||||
@@ -984,6 +1091,36 @@ public class CategoriesImplTest
|
||||
.isEqualTo(expectedLinkedCategories);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLinkNodeToCategories_includePath()
|
||||
{
|
||||
final NodeRef categoryParentNodeRef = createNodeRefWithId(PARENT_ID);
|
||||
final ChildAssociationRef parentAssociation = createAssociationOf(categoryParentNodeRef, CATEGORY_NODE_REF);
|
||||
given(nodesMock.getNode(any())).willReturn(createNode());
|
||||
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
|
||||
given(nodeServiceMock.hasAspect(any(), any())).willReturn(true);
|
||||
given(parametersMock.getInclude()).willReturn(List.of(Nodes.PARAM_INCLUDE_PATH));
|
||||
given(nodeServiceMock.getPath(any())).willReturn(mockCategoryPath());
|
||||
|
||||
// when
|
||||
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY), parametersMock);
|
||||
|
||||
then(nodesMock).should(times(2)).getNode(CATEGORY_ID);
|
||||
then(nodeServiceMock).should().getChildAssocs(CATEGORY_NODE_REF, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false);
|
||||
then(nodeServiceMock).should().getPrimaryParent(CATEGORY_NODE_REF);
|
||||
then(nodeServiceMock).should().getParentAssocs(CATEGORY_NODE_REF);
|
||||
then(nodeServiceMock).should().hasAspect(CONTENT_NODE_REF, ContentModel.ASPECT_GEN_CLASSIFIABLE);
|
||||
then(nodeServiceMock).should().getProperty(CONTENT_NODE_REF, ContentModel.PROP_CATEGORIES);
|
||||
final Serializable expectedCategories = (Serializable) List.of(CATEGORY_NODE_REF);
|
||||
then(nodeServiceMock).should().setProperty(CONTENT_NODE_REF, ContentModel.PROP_CATEGORIES, expectedCategories);
|
||||
then(nodeServiceMock).should().getParentAssocs(categoryParentNodeRef);
|
||||
final List<Category> expectedLinkedCategories = List.of(CATEGORY);
|
||||
expectedLinkedCategories.get(0).setPath(MOCK_CATEGORY_PATH);
|
||||
assertThat(actualLinkedCategories)
|
||||
.isNotNull().usingRecursiveComparison()
|
||||
.isEqualTo(expectedLinkedCategories);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLinkNodeToCategories_withPreviouslyLinkedCategories()
|
||||
{
|
||||
@@ -1168,11 +1305,46 @@ public class CategoriesImplTest
|
||||
then(nodeServiceMock).should().getParentAssocs(categoryParentNodeRef);
|
||||
then(nodeServiceMock).shouldHaveNoMoreInteractions();
|
||||
final List<Category> expectedCategories = List.of(CATEGORY);
|
||||
expectedCategories.get(0).setPath(null);
|
||||
assertThat(actualCategories)
|
||||
.isNotNull().usingRecursiveComparison()
|
||||
.isEqualTo(expectedCategories);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListCategoriesForNode_includePath()
|
||||
{
|
||||
final NodeRef categoryParentNodeRef = createNodeRefWithId(PARENT_ID);
|
||||
final ChildAssociationRef parentAssociation = createAssociationOf(categoryParentNodeRef, CATEGORY_NODE_REF);
|
||||
given(nodeServiceMock.getProperty(any(), eq(ContentModel.PROP_CATEGORIES))).willReturn((Serializable) List.of(CATEGORY_NODE_REF));
|
||||
given(nodesMock.getNode(any())).willReturn(createNode());
|
||||
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
|
||||
given(parametersMock.getInclude()).willReturn(List.of(Nodes.PARAM_INCLUDE_PATH));
|
||||
given(nodeServiceMock.getPath(any())).willReturn(mockCategoryPath());
|
||||
|
||||
// when
|
||||
final List<Category> actualCategories = objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID, parametersMock);
|
||||
|
||||
then(nodesMock).should().validateOrLookupNode(CONTENT_NODE_ID);
|
||||
then(permissionServiceMock).should().hasReadPermission(CONTENT_NODE_REF);
|
||||
then(permissionServiceMock).shouldHaveNoMoreInteractions();
|
||||
then(typeConstraint).should().matches(CONTENT_NODE_REF);
|
||||
then(typeConstraint).shouldHaveNoMoreInteractions();
|
||||
then(nodesMock).should(times(2)).getNode(CATEGORY_ID);
|
||||
then(nodesMock).shouldHaveNoMoreInteractions();
|
||||
then(nodeServiceMock).should().getProperty(CONTENT_NODE_REF, ContentModel.PROP_CATEGORIES);
|
||||
then(nodeServiceMock).should().getChildAssocs(CATEGORY_NODE_REF, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false);
|
||||
then(nodeServiceMock).should().getPrimaryParent(CATEGORY_NODE_REF);
|
||||
then(nodeServiceMock).should().getParentAssocs(categoryParentNodeRef);
|
||||
then(nodeServiceMock).should().getPath(any());
|
||||
then(nodeServiceMock).shouldHaveNoMoreInteractions();
|
||||
final List<Category> expectedCategories = List.of(CATEGORY);
|
||||
expectedCategories.get(0).setPath(MOCK_CATEGORY_PATH);
|
||||
assertThat(actualCategories)
|
||||
.isNotNull().usingRecursiveComparison()
|
||||
.isEqualTo(expectedCategories);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListCategoriesForNode_withInvalidNodeId()
|
||||
{
|
||||
@@ -1329,4 +1501,13 @@ public class CategoriesImplTest
|
||||
{
|
||||
return new ChildAssociationRef(ContentModel.ASSOC_SUBCATEGORIES, parentNode, childNodeName, childNode);
|
||||
}
|
||||
|
||||
private Path mockCategoryPath()
|
||||
{
|
||||
final Path mockPath = new Path();
|
||||
mockPath.append(PathHelper.stringToPath(MOCK_ROOT_LEVEL));
|
||||
mockPath.append(PathHelper.stringToPath(MOCK_CHILD_LEVEL));
|
||||
mockPath.append(PathHelper.stringToPath("/"));
|
||||
return mockPath;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user