MIFOSX-321: tidy up some of backend implementation for client pagination and sorting example.
This commit is contained in:
+5
-16
@@ -3,36 +3,25 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/.
|
* You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
package org.mifosplatform.portfolio.pagination;
|
package org.mifosplatform.infrastructure.core.service;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
|
|
||||||
public class Page<E> {
|
public class Page<E> {
|
||||||
|
|
||||||
private int totalFilteredRecords;
|
private final int totalFilteredRecords;
|
||||||
private List<E> pageItems = new ArrayList<E>();
|
private final List<E> pageItems;
|
||||||
|
|
||||||
public void setPageItems(List<E> pageItems) {
|
public Page(final List<E> pageItems, final int totalFilteredRecords) {
|
||||||
this.pageItems = pageItems;
|
this.pageItems = pageItems;
|
||||||
|
this.totalFilteredRecords = totalFilteredRecords;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getTotalFilteredRecords() {
|
public int getTotalFilteredRecords() {
|
||||||
return this.totalFilteredRecords;
|
return this.totalFilteredRecords;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTotalFilteredRecords(int totalFilteredRecords) {
|
|
||||||
this.totalFilteredRecords = totalFilteredRecords;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<E> getPageItems() {
|
public List<E> getPageItems() {
|
||||||
return pageItems;
|
return pageItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return new Gson().toJson(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+5
-8
@@ -3,7 +3,9 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/.
|
* You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
package org.mifosplatform.portfolio.pagination;
|
package org.mifosplatform.infrastructure.core.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.jdbc.core.RowMapper;
|
import org.springframework.jdbc.core.RowMapper;
|
||||||
@@ -13,16 +15,11 @@ public class PaginationHelper<E> {
|
|||||||
public Page<E> fetchPage(final JdbcTemplate jt, final String sqlCountRows, final String sqlFetchRows, final Object args[],
|
public Page<E> fetchPage(final JdbcTemplate jt, final String sqlCountRows, final String sqlFetchRows, final Object args[],
|
||||||
final RowMapper<E> rowMapper) {
|
final RowMapper<E> rowMapper) {
|
||||||
|
|
||||||
final Page<E> page = new Page<E>();
|
final List<E> items = jt.query(sqlFetchRows, args, rowMapper);
|
||||||
|
|
||||||
page.getPageItems().addAll(jt.query(sqlFetchRows, args, rowMapper));
|
|
||||||
|
|
||||||
// determine how many rows are available
|
// determine how many rows are available
|
||||||
final int totalFilteredRecords = jt.queryForInt(sqlCountRows);
|
final int totalFilteredRecords = jt.queryForInt(sqlCountRows);
|
||||||
|
|
||||||
page.setTotalFilteredRecords(totalFilteredRecords);
|
return new Page<E>(items, totalFilteredRecords);
|
||||||
|
|
||||||
return page;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+10
-9
@@ -33,6 +33,7 @@ import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
|
|||||||
import org.mifosplatform.infrastructure.core.exception.UnrecognizedQueryParamException;
|
import org.mifosplatform.infrastructure.core.exception.UnrecognizedQueryParamException;
|
||||||
import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||||
import org.mifosplatform.infrastructure.core.serialization.ToApiJsonSerializer;
|
import org.mifosplatform.infrastructure.core.serialization.ToApiJsonSerializer;
|
||||||
|
import org.mifosplatform.infrastructure.core.service.Page;
|
||||||
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
|
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.mifosplatform.organisation.office.data.OfficeData;
|
import org.mifosplatform.organisation.office.data.OfficeData;
|
||||||
import org.mifosplatform.organisation.office.service.OfficeReadPlatformService;
|
import org.mifosplatform.organisation.office.service.OfficeReadPlatformService;
|
||||||
@@ -40,7 +41,6 @@ import org.mifosplatform.portfolio.client.data.ClientAccountSummaryCollectionDat
|
|||||||
import org.mifosplatform.portfolio.client.data.ClientData;
|
import org.mifosplatform.portfolio.client.data.ClientData;
|
||||||
import org.mifosplatform.portfolio.client.service.ClientReadPlatformService;
|
import org.mifosplatform.portfolio.client.service.ClientReadPlatformService;
|
||||||
import org.mifosplatform.portfolio.group.service.SearchParameters;
|
import org.mifosplatform.portfolio.group.service.SearchParameters;
|
||||||
import org.mifosplatform.portfolio.pagination.Page;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Scope;
|
import org.springframework.context.annotation.Scope;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -90,18 +90,19 @@ public class ClientsApiResource {
|
|||||||
@GET
|
@GET
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
public String retrieveAllPaginatedAndSorted(@QueryParam("sqlSearch") final String sqlSearch,
|
public String retrieveAll(@QueryParam("sqlSearch") final String sqlSearch, @QueryParam("officeId") final Long officeId,
|
||||||
@QueryParam("officeId") final Long officeId, @QueryParam("externalId") final String externalId,
|
@QueryParam("externalId") final String externalId, @QueryParam("displayName") final String displayName,
|
||||||
@QueryParam("displayName") final String displayName, @QueryParam("firstName") final String firstname,
|
@QueryParam("firstName") final String firstname, @QueryParam("lastName") final String lastname,
|
||||||
@QueryParam("lastName") final String lastname, @QueryParam("underHierarchy") final String hierarchy,
|
@QueryParam("underHierarchy") final String hierarchy, @QueryParam("offset") final Integer offset,
|
||||||
@QueryParam("offset") final int offset, @QueryParam("limit") final int limit, @QueryParam("orderBy") final String orderBy,
|
@QueryParam("limit") final Integer limit, @QueryParam("orderBy") final String orderBy,
|
||||||
@QueryParam("sortOrder") final String sortOrder) {
|
@QueryParam("sortOrder") final String sortOrder) {
|
||||||
|
|
||||||
context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||||
|
|
||||||
final SearchParameters searchParameters = SearchParameters.forClients(sqlSearch, officeId, externalId, displayName, firstname,
|
final SearchParameters searchParameters = SearchParameters.forClients(sqlSearch, officeId, externalId, displayName, firstname,
|
||||||
lastname, hierarchy);
|
lastname, hierarchy, offset, limit, orderBy, sortOrder);
|
||||||
Page<ClientData> clientData = this.clientReadPlatformService.retrieveAllPaginatedAndSorted(searchParameters, offset, limit,
|
|
||||||
orderBy, sortOrder);
|
final Page<ClientData> clientData = this.clientReadPlatformService.retrieveAll(searchParameters);
|
||||||
return this.toApiJsonSerializer.serialize(clientData);
|
return this.toApiJsonSerializer.serialize(clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-4
@@ -7,19 +7,17 @@ package org.mifosplatform.portfolio.client.service;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import org.mifosplatform.infrastructure.core.service.Page;
|
||||||
import org.mifosplatform.portfolio.client.data.ClientAccountSummaryCollectionData;
|
import org.mifosplatform.portfolio.client.data.ClientAccountSummaryCollectionData;
|
||||||
import org.mifosplatform.portfolio.client.data.ClientAccountSummaryData;
|
import org.mifosplatform.portfolio.client.data.ClientAccountSummaryData;
|
||||||
import org.mifosplatform.portfolio.client.data.ClientData;
|
import org.mifosplatform.portfolio.client.data.ClientData;
|
||||||
import org.mifosplatform.portfolio.group.service.SearchParameters;
|
import org.mifosplatform.portfolio.group.service.SearchParameters;
|
||||||
import org.mifosplatform.portfolio.pagination.Page;
|
|
||||||
|
|
||||||
public interface ClientReadPlatformService {
|
public interface ClientReadPlatformService {
|
||||||
|
|
||||||
ClientData retrieveTemplate();
|
ClientData retrieveTemplate();
|
||||||
|
|
||||||
Collection<ClientData> retrieveAll(SearchParameters searchParameters);
|
Page<ClientData> retrieveAll(SearchParameters searchParameters);
|
||||||
|
|
||||||
Page<ClientData> retrieveAllPaginatedAndSorted(SearchParameters searchParameters, int offset, int limit, String sortBy, String sortOrder);
|
|
||||||
|
|
||||||
ClientData retrieveOne(Long clientId);
|
ClientData retrieveOne(Long clientId);
|
||||||
|
|
||||||
|
|||||||
+26
-33
@@ -16,6 +16,8 @@ import org.joda.time.LocalDate;
|
|||||||
import org.mifosplatform.infrastructure.core.api.ApiParameterHelper;
|
import org.mifosplatform.infrastructure.core.api.ApiParameterHelper;
|
||||||
import org.mifosplatform.infrastructure.core.data.EnumOptionData;
|
import org.mifosplatform.infrastructure.core.data.EnumOptionData;
|
||||||
import org.mifosplatform.infrastructure.core.domain.JdbcSupport;
|
import org.mifosplatform.infrastructure.core.domain.JdbcSupport;
|
||||||
|
import org.mifosplatform.infrastructure.core.service.Page;
|
||||||
|
import org.mifosplatform.infrastructure.core.service.PaginationHelper;
|
||||||
import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource;
|
import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource;
|
||||||
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
|
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.mifosplatform.organisation.office.data.OfficeData;
|
import org.mifosplatform.organisation.office.data.OfficeData;
|
||||||
@@ -29,8 +31,6 @@ import org.mifosplatform.portfolio.group.data.GroupGeneralData;
|
|||||||
import org.mifosplatform.portfolio.group.service.SearchParameters;
|
import org.mifosplatform.portfolio.group.service.SearchParameters;
|
||||||
import org.mifosplatform.portfolio.loanaccount.data.LoanStatusEnumData;
|
import org.mifosplatform.portfolio.loanaccount.data.LoanStatusEnumData;
|
||||||
import org.mifosplatform.portfolio.loanproduct.service.LoanEnumerations;
|
import org.mifosplatform.portfolio.loanproduct.service.LoanEnumerations;
|
||||||
import org.mifosplatform.portfolio.pagination.Page;
|
|
||||||
import org.mifosplatform.portfolio.pagination.PaginationHelper;
|
|
||||||
import org.mifosplatform.useradministration.domain.AppUser;
|
import org.mifosplatform.useradministration.domain.AppUser;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.dao.EmptyResultDataAccessException;
|
import org.springframework.dao.EmptyResultDataAccessException;
|
||||||
@@ -46,6 +46,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
|||||||
private final OfficeReadPlatformService officeReadPlatformService;
|
private final OfficeReadPlatformService officeReadPlatformService;
|
||||||
|
|
||||||
// data mappers
|
// data mappers
|
||||||
|
private final PaginationHelper<ClientData> paginationHelper = new PaginationHelper<ClientData>();
|
||||||
private final ClientMapper clientMapper = new ClientMapper();
|
private final ClientMapper clientMapper = new ClientMapper();
|
||||||
private final ClientLookupMapper lookupMapper = new ClientLookupMapper();
|
private final ClientLookupMapper lookupMapper = new ClientLookupMapper();
|
||||||
private final ClientMembersOfGroupMapper membersOfGroupMapper = new ClientMembersOfGroupMapper();
|
private final ClientMembersOfGroupMapper membersOfGroupMapper = new ClientMembersOfGroupMapper();
|
||||||
@@ -72,49 +73,41 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<ClientData> retrieveAll(final SearchParameters searchParameters) {
|
public Page<ClientData> retrieveAll(final SearchParameters searchParameters) {
|
||||||
|
|
||||||
final AppUser currentUser = context.authenticatedUser();
|
final AppUser currentUser = context.authenticatedUser();
|
||||||
final String hierarchy = currentUser.getOffice().getHierarchy();
|
final String hierarchy = currentUser.getOffice().getHierarchy();
|
||||||
final String hierarchySearchString = hierarchy + "%";
|
final String hierarchySearchString = hierarchy + "%";
|
||||||
|
|
||||||
String sql = "select " + this.clientMapper.schema() + " where o.hierarchy like ?";
|
StringBuilder sqlBuilder = new StringBuilder(200);
|
||||||
|
sqlBuilder.append("select SQL_CALC_FOUND_ROWS ");
|
||||||
|
sqlBuilder.append(this.clientMapper.schema());
|
||||||
|
sqlBuilder.append(" where o.hierarchy like ?");
|
||||||
|
|
||||||
final String extraCriteria = buildSqlStringFromClientCriteria(searchParameters);
|
final String extraCriteria = buildSqlStringFromClientCriteria(searchParameters);
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(extraCriteria)) sql += " and (" + extraCriteria + ")";
|
if (StringUtils.isNotBlank(extraCriteria)) {
|
||||||
|
sqlBuilder.append(" and (").append(extraCriteria).append(")");
|
||||||
|
}
|
||||||
|
|
||||||
sql += " order by c.display_name ASC, c.account_no ASC";
|
// DONT order by default - just use database natural ordering so doesnt
|
||||||
|
// have to scan entire database table.
|
||||||
|
// sql += " order by c.display_name ASC, c.account_no ASC";
|
||||||
|
if (searchParameters.isOrderByRequested()) {
|
||||||
|
sqlBuilder.append(" order by ").append(searchParameters.getOrderBy()).append(' ').append(searchParameters.getSortOrder());
|
||||||
|
}
|
||||||
|
|
||||||
return this.jdbcTemplate.query(sql, clientMapper, new Object[] { hierarchySearchString });
|
if (searchParameters.isLimited()) {
|
||||||
}
|
sqlBuilder.append(" limit ").append(searchParameters.getLimit());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
if (searchParameters.isOffset()) {
|
||||||
public Page<ClientData> retrieveAllPaginatedAndSorted(final SearchParameters searchParameters, final int offset, final int limit,
|
sqlBuilder.append(" offset ").append(searchParameters.getOffset());
|
||||||
final String orderby, final String sortOrder) {
|
}
|
||||||
|
|
||||||
final AppUser currentUser = context.authenticatedUser();
|
final String sqlCountRows = "SELECT FOUND_ROWS()";
|
||||||
final String hierarchy = currentUser.getOffice().getHierarchy();
|
return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlCountRows, sqlBuilder.toString(),
|
||||||
final String hierarchySearchString = hierarchy + "%";
|
new Object[] { hierarchySearchString }, this.clientMapper);
|
||||||
|
|
||||||
String sql = "select SQL_CALC_FOUND_ROWS " + this.clientMapper.schema() + " where o.hierarchy like ?";
|
|
||||||
|
|
||||||
final String extraCriteria = buildSqlStringFromClientCriteria(searchParameters);
|
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(extraCriteria)) sql += " and (" + extraCriteria + ")";
|
|
||||||
|
|
||||||
if (orderby != null && sortOrder != null) {
|
|
||||||
if (!orderby.equalsIgnoreCase("") && !sortOrder.equalsIgnoreCase("")) {
|
|
||||||
sql += " order by " + orderby + " " + sortOrder;
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
sql += " order by c.display_name ASC, c.account_no ASC";
|
|
||||||
|
|
||||||
PaginationHelper<ClientData> ph = new PaginationHelper<ClientData>();
|
|
||||||
String sqlCountRows = "SELECT FOUND_ROWS()";
|
|
||||||
String sqlFetchRows = sql;
|
|
||||||
if (limit != 0) sqlFetchRows = sql + " LIMIT " + limit + " OFFSET " + offset;
|
|
||||||
return ph.fetchPage(jdbcTemplate, sqlCountRows, sqlFetchRows, new Object[] { hierarchySearchString }, clientMapper);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildSqlStringFromClientCriteria(final SearchParameters searchParameters) {
|
private String buildSqlStringFromClientCriteria(final SearchParameters searchParameters) {
|
||||||
|
|||||||
+52
-5
@@ -1,6 +1,8 @@
|
|||||||
package org.mifosplatform.portfolio.group.service;
|
package org.mifosplatform.portfolio.group.service;
|
||||||
|
|
||||||
public class SearchParameters {
|
import org.apache.commons.lang.StringUtils;
|
||||||
|
|
||||||
|
public final class SearchParameters {
|
||||||
|
|
||||||
private final String sqlSearch;
|
private final String sqlSearch;
|
||||||
private final Long officeId;
|
private final Long officeId;
|
||||||
@@ -9,19 +11,32 @@ public class SearchParameters {
|
|||||||
private final String hierarchy;
|
private final String hierarchy;
|
||||||
private final String firstname;
|
private final String firstname;
|
||||||
private final String lastname;
|
private final String lastname;
|
||||||
|
private final Integer offset;
|
||||||
|
private final Integer limit;
|
||||||
|
private final String orderBy;
|
||||||
|
private final String sortOrder;
|
||||||
|
|
||||||
public static SearchParameters from(final String sqlSearch, final Long officeId, final String externalId, final String name,
|
public static SearchParameters from(final String sqlSearch, final Long officeId, final String externalId, final String name,
|
||||||
final String hierarchy) {
|
final String hierarchy) {
|
||||||
return new SearchParameters(sqlSearch, officeId, externalId, name, hierarchy, null, null);
|
return new SearchParameters(sqlSearch, officeId, externalId, name, hierarchy, null, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SearchParameters forClients(final String sqlSearch, final Long officeId, final String externalId,
|
public static SearchParameters forClients(final String sqlSearch, final Long officeId, final String externalId,
|
||||||
final String displayName, final String firstname, final String lastname, final String hierarchy) {
|
final String displayName, final String firstname, final String lastname, final String hierarchy, final Integer offset,
|
||||||
return new SearchParameters(sqlSearch, officeId, externalId, displayName, hierarchy, firstname, lastname);
|
final Integer limit, final String orderBy, final String sortOrder) {
|
||||||
|
|
||||||
|
Integer maxLimitAllowed = 200;
|
||||||
|
if (limit != null && limit < maxLimitAllowed && limit > 0) {
|
||||||
|
maxLimitAllowed = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SearchParameters(sqlSearch, officeId, externalId, displayName, hierarchy, firstname, lastname, offset, maxLimitAllowed,
|
||||||
|
orderBy, sortOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private SearchParameters(final String sqlSearch, final Long officeId, final String externalId, final String name,
|
private SearchParameters(final String sqlSearch, final Long officeId, final String externalId, final String name,
|
||||||
final String hierarchy, final String firstname, final String lastname) {
|
final String hierarchy, final String firstname, final String lastname, final Integer offset, final Integer limit,
|
||||||
|
final String orderBy, final String sortOrder) {
|
||||||
this.sqlSearch = sqlSearch;
|
this.sqlSearch = sqlSearch;
|
||||||
this.officeId = officeId;
|
this.officeId = officeId;
|
||||||
this.externalId = externalId;
|
this.externalId = externalId;
|
||||||
@@ -29,6 +44,14 @@ public class SearchParameters {
|
|||||||
this.hierarchy = hierarchy;
|
this.hierarchy = hierarchy;
|
||||||
this.firstname = firstname;
|
this.firstname = firstname;
|
||||||
this.lastname = lastname;
|
this.lastname = lastname;
|
||||||
|
this.offset = offset;
|
||||||
|
this.limit = limit;
|
||||||
|
this.orderBy = orderBy;
|
||||||
|
this.sortOrder = sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOrderByRequested() {
|
||||||
|
return StringUtils.isNotBlank(this.orderBy) && StringUtils.isNotBlank(this.sortOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSqlSearch() {
|
public String getSqlSearch() {
|
||||||
@@ -58,4 +81,28 @@ public class SearchParameters {
|
|||||||
public String getLastname() {
|
public String getLastname() {
|
||||||
return this.lastname;
|
return this.lastname;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getOffset() {
|
||||||
|
return this.offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getLimit() {
|
||||||
|
return this.limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOrderBy() {
|
||||||
|
return this.orderBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSortOrder() {
|
||||||
|
return this.sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLimited() {
|
||||||
|
return this.limit != null && this.limit.intValue() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOffset() {
|
||||||
|
return this.offset != null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user