mirror of
https://github.com/hapifhir/hapi-fhir.git
synced 2025-03-09 14:33:32 +00:00
Work on presence API
This commit is contained in:
parent
03fc593cb9
commit
bb637c5433
@ -326,7 +326,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
|
|||||||
});
|
});
|
||||||
txTemplate.execute(t -> {
|
txTemplate.execute(t -> {
|
||||||
doExpungeEverythingQuery("DELETE from " + SearchParamPresent.class.getSimpleName() + " d");
|
doExpungeEverythingQuery("DELETE from " + SearchParamPresent.class.getSimpleName() + " d");
|
||||||
doExpungeEverythingQuery("DELETE from " + SearchParam.class.getSimpleName() + " d");
|
|
||||||
doExpungeEverythingQuery("DELETE from " + ForcedId.class.getSimpleName() + " d");
|
doExpungeEverythingQuery("DELETE from " + ForcedId.class.getSimpleName() + " d");
|
||||||
doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamDate.class.getSimpleName() + " d");
|
doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamDate.class.getSimpleName() + " d");
|
||||||
doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamNumber.class.getSimpleName() + " d");
|
doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamNumber.class.getSimpleName() + " d");
|
||||||
|
@ -297,11 +297,10 @@ public class SearchBuilder implements ISearchBuilder {
|
|||||||
|
|
||||||
private void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing) {
|
private void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing) {
|
||||||
Join<ResourceTable, SearchParamPresent> paramPresentJoin = myResourceTableRoot.join("mySearchParamPresents", JoinType.LEFT);
|
Join<ResourceTable, SearchParamPresent> paramPresentJoin = myResourceTableRoot.join("mySearchParamPresents", JoinType.LEFT);
|
||||||
Join<SearchParamPresent, SearchParam> paramJoin = paramPresentJoin.join("mySearchParam", JoinType.LEFT);
|
|
||||||
|
|
||||||
myPredicates.add(myBuilder.equal(paramJoin.get("myResourceName"), theResourceName));
|
Expression<Long> hashPresence = paramPresentJoin.get("myHashPresence").as(Long.class);
|
||||||
myPredicates.add(myBuilder.equal(paramJoin.get("myParamName"), theParamName));
|
Long hash = SearchParamPresent.calculateHashPresence(theResourceName, theParamName, !theMissing);
|
||||||
myPredicates.add(myBuilder.equal(paramPresentJoin.get("myPresent"), !theMissing));
|
myPredicates.add(myBuilder.equal(hashPresence, hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing, Join<ResourceTable, ? extends BaseResourceIndexedSearchParam> theJoin) {
|
private void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing, Join<ResourceTable, ? extends BaseResourceIndexedSearchParam> theJoin) {
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
package ca.uhn.fhir.jpa.dao.data;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* #%L
|
|
||||||
* HAPI FHIR JPA Server
|
|
||||||
* %%
|
|
||||||
* Copyright (C) 2014 - 2018 University Health Network
|
|
||||||
* %%
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
* #L%
|
|
||||||
*/
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.entity.SearchParam;
|
|
||||||
|
|
||||||
public interface ISearchParamDao extends JpaRepository<SearchParam, Long> {
|
|
||||||
|
|
||||||
@Query("SELECT s FROM SearchParam s WHERE s.myResourceName = :resname AND s.myParamName = :parmname")
|
|
||||||
public SearchParam findForResource(@Param("resname") String theResourceType, @Param("parmname") String theParamName);
|
|
||||||
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
package ca.uhn.fhir.jpa.entity;
|
|
||||||
|
|
||||||
/*-
|
|
||||||
* #%L
|
|
||||||
* HAPI FHIR JPA Server
|
|
||||||
* %%
|
|
||||||
* Copyright (C) 2014 - 2018 University Health Network
|
|
||||||
* %%
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
* #L%
|
|
||||||
*/
|
|
||||||
|
|
||||||
import javax.persistence.*;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "HFJ_SEARCH_PARM", uniqueConstraints= {
|
|
||||||
@UniqueConstraint(name="IDX_SEARCHPARM_RESTYPE_SPNAME", columnNames= {"RES_TYPE", "PARAM_NAME"})
|
|
||||||
})
|
|
||||||
public class SearchParam {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@SequenceGenerator(name = "SEQ_SEARCHPARM_ID", sequenceName = "SEQ_SEARCHPARM_ID")
|
|
||||||
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SEARCHPARM_ID")
|
|
||||||
@Column(name = "PID")
|
|
||||||
private Long myId;
|
|
||||||
|
|
||||||
@Column(name="PARAM_NAME", length=BaseResourceIndexedSearchParam.MAX_SP_NAME, nullable=false, updatable=false)
|
|
||||||
private String myParamName;
|
|
||||||
|
|
||||||
@Column(name="RES_TYPE", length=ResourceTable.RESTYPE_LEN, nullable=false, updatable=false)
|
|
||||||
private String myResourceName;
|
|
||||||
|
|
||||||
public String getParamName() {
|
|
||||||
return myParamName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setParamName(String theParamName) {
|
|
||||||
myParamName = theParamName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setResourceName(String theResourceName) {
|
|
||||||
myResourceName = theResourceName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() {
|
|
||||||
return myId;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -20,18 +20,16 @@ package ca.uhn.fhir.jpa.entity;
|
|||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
import javax.persistence.*;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "HFJ_RES_PARAM_PRESENT", indexes = {
|
@Table(name = "HFJ_RES_PARAM_PRESENT", indexes = {
|
||||||
@Index(name = "IDX_RESPARMPRESENT_RESID", columnList = "RES_ID")
|
@Index(name = "IDX_RESPARMPRESENT_RESID", columnList = "RES_ID"),
|
||||||
}, uniqueConstraints = {
|
@Index(name = "IDX_RESPARMPRESENT_HASHPRES", columnList = "HASH_PRESENCE")
|
||||||
@UniqueConstraint(name = "IDX_RESPARMPRESENT_SPID_RESID", columnNames = { "SP_ID", "RES_ID" })
|
|
||||||
})
|
})
|
||||||
public class SearchParamPresent implements Serializable {
|
public class SearchParamPresent implements Serializable {
|
||||||
|
|
||||||
@ -42,17 +40,15 @@ public class SearchParamPresent implements Serializable {
|
|||||||
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_RESPARMPRESENT_ID")
|
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_RESPARMPRESENT_ID")
|
||||||
@Column(name = "PID")
|
@Column(name = "PID")
|
||||||
private Long myId;
|
private Long myId;
|
||||||
|
|
||||||
@Column(name = "SP_PRESENT", nullable = false)
|
@Column(name = "SP_PRESENT", nullable = false)
|
||||||
private boolean myPresent;
|
private boolean myPresent;
|
||||||
|
|
||||||
@ManyToOne()
|
@ManyToOne()
|
||||||
@JoinColumn(name = "RES_ID", referencedColumnName = "RES_ID", nullable = false, foreignKey = @ForeignKey(name = "FK_RESPARMPRES_RESID"))
|
@JoinColumn(name = "RES_ID", referencedColumnName = "RES_ID", nullable = false, foreignKey = @ForeignKey(name = "FK_RESPARMPRES_RESID"))
|
||||||
private ResourceTable myResource;
|
private ResourceTable myResource;
|
||||||
|
@Transient
|
||||||
@ManyToOne()
|
private transient String myParamName;
|
||||||
@JoinColumn(name = "SP_ID", referencedColumnName = "PID", nullable = false, foreignKey = @ForeignKey(name = "FK_RESPARMPRES_SPID"))
|
@Column(name = "HASH_PRESENCE")
|
||||||
private SearchParam mySearchParam;
|
private Long myHashPresence;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
@ -61,12 +57,39 @@ public class SearchParamPresent implements Serializable {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
@PrePersist
|
||||||
|
public void calculateHashes() {
|
||||||
|
if (myHashPresence == null) {
|
||||||
|
String resourceType = getResource().getResourceType();
|
||||||
|
String paramName = getParamName();
|
||||||
|
boolean present = myPresent;
|
||||||
|
setHashPresence(calculateHashPresence(resourceType, paramName, present));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getHashPresence() {
|
||||||
|
return myHashPresence;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHashPresence(Long theHashPresence) {
|
||||||
|
myHashPresence = theHashPresence;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getParamName() {
|
||||||
|
return myParamName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setParamName(String theParamName) {
|
||||||
|
myParamName = theParamName;
|
||||||
|
}
|
||||||
|
|
||||||
public ResourceTable getResource() {
|
public ResourceTable getResource() {
|
||||||
return myResource;
|
return myResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SearchParam getSearchParam() {
|
public void setResource(ResourceTable theResourceTable) {
|
||||||
return mySearchParam;
|
myResource = theResourceTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPresent() {
|
public boolean isPresent() {
|
||||||
@ -77,22 +100,18 @@ public class SearchParamPresent implements Serializable {
|
|||||||
myPresent = thePresent;
|
myPresent = thePresent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setResource(ResourceTable theResourceTable) {
|
|
||||||
myResource = theResourceTable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSearchParam(SearchParam theSearchParam) {
|
|
||||||
mySearchParam = theSearchParam;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
|
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
|
||||||
|
|
||||||
b.append("res_pid", myResource.getIdDt().toUnqualifiedVersionless().getValue());
|
b.append("resPid", myResource.getIdDt().toUnqualifiedVersionless().getValue());
|
||||||
b.append("param", mySearchParam.getParamName());
|
b.append("paramName", myParamName);
|
||||||
b.append("present", myPresent);
|
b.append("present", myPresent);
|
||||||
return b.build();
|
return b.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static long calculateHashPresence(String theResourceType, String theParamName, boolean thePresent) {
|
||||||
|
return BaseResourceIndexedSearchParam.hash(theResourceType, theParamName, Boolean.toString(thePresent));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,14 +20,12 @@ package ca.uhn.fhir.jpa.sp;
|
|||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.entity.ResourceTable;
|
import ca.uhn.fhir.jpa.entity.ResourceTable;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public interface ISearchParamPresenceSvc {
|
public interface ISearchParamPresenceSvc {
|
||||||
|
|
||||||
void updatePresence(ResourceTable theResource, Map<String, Boolean> theParamNameToPresence);
|
void updatePresence(ResourceTable theResource, Map<String, Boolean> theParamNameToPresence);
|
||||||
|
|
||||||
void flushCachesForUnitTest();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,29 +20,17 @@ package ca.uhn.fhir.jpa.sp;
|
|||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.dao.data.ISearchParamDao;
|
|
||||||
import ca.uhn.fhir.jpa.dao.data.ISearchParamPresentDao;
|
import ca.uhn.fhir.jpa.dao.data.ISearchParamPresentDao;
|
||||||
import ca.uhn.fhir.jpa.entity.ResourceTable;
|
import ca.uhn.fhir.jpa.entity.ResourceTable;
|
||||||
import ca.uhn.fhir.jpa.entity.SearchParam;
|
|
||||||
import ca.uhn.fhir.jpa.entity.SearchParamPresent;
|
import ca.uhn.fhir.jpa.entity.SearchParamPresent;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
public class SearchParamPresenceSvcImpl implements ISearchParamPresenceSvc {
|
public class SearchParamPresenceSvcImpl implements ISearchParamPresenceSvc {
|
||||||
|
|
||||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamPresenceSvcImpl.class);
|
|
||||||
|
|
||||||
private Map<Pair<String, String>, SearchParam> myResourceTypeToSearchParamToEntity = new ConcurrentHashMap<Pair<String, String>, SearchParam>();
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private ISearchParamDao mySearchParamDao;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ISearchParamPresentDao mySearchParamPresentDao;
|
private ISearchParamPresentDao mySearchParamPresentDao;
|
||||||
|
|
||||||
@ -55,62 +43,48 @@ public class SearchParamPresenceSvcImpl implements ISearchParamPresenceSvc {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Boolean> presenceMap = new HashMap<String, Boolean>(theParamNameToPresence);
|
Map<String, Boolean> presenceMap = new HashMap<>(theParamNameToPresence);
|
||||||
List<SearchParamPresent> entitiesToSave = new ArrayList<SearchParamPresent>();
|
|
||||||
List<SearchParamPresent> entitiesToDelete = new ArrayList<SearchParamPresent>();
|
|
||||||
|
|
||||||
|
// Find existing entries
|
||||||
Collection<SearchParamPresent> existing;
|
Collection<SearchParamPresent> existing;
|
||||||
existing = mySearchParamPresentDao.findAllForResource(theResource);
|
existing = mySearchParamPresentDao.findAllForResource(theResource);
|
||||||
|
Map<Long, SearchParamPresent> existingHashToPresence = new HashMap<>();
|
||||||
for (SearchParamPresent nextExistingEntity : existing) {
|
for (SearchParamPresent nextExistingEntity : existing) {
|
||||||
String nextSearchParamName = nextExistingEntity.getSearchParam().getParamName();
|
existingHashToPresence.put(nextExistingEntity.getHashPresence(), nextExistingEntity);
|
||||||
Boolean existingValue = presenceMap.remove(nextSearchParamName);
|
|
||||||
if (existingValue == null) {
|
|
||||||
entitiesToDelete.add(nextExistingEntity);
|
|
||||||
} else if (existingValue.booleanValue() == nextExistingEntity.isPresent()) {
|
|
||||||
ourLog.trace("No change for search param {}", nextSearchParamName);
|
|
||||||
} else {
|
|
||||||
nextExistingEntity.setPresent(existingValue);
|
|
||||||
entitiesToSave.add(nextExistingEntity);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find newly wanted set of entries
|
||||||
|
Map<Long, SearchParamPresent> newHashToPresence = new HashMap<>();
|
||||||
for (Entry<String, Boolean> next : presenceMap.entrySet()) {
|
for (Entry<String, Boolean> next : presenceMap.entrySet()) {
|
||||||
String resourceType = theResource.getResourceType();
|
|
||||||
String paramName = next.getKey();
|
String paramName = next.getKey();
|
||||||
Pair<String, String> key = Pair.of(resourceType, paramName);
|
|
||||||
|
|
||||||
SearchParam searchParam = myResourceTypeToSearchParamToEntity.get(key);
|
|
||||||
if (searchParam == null) {
|
|
||||||
searchParam = mySearchParamDao.findForResource(resourceType, paramName);
|
|
||||||
if (searchParam != null) {
|
|
||||||
myResourceTypeToSearchParamToEntity.put(key, searchParam);
|
|
||||||
} else {
|
|
||||||
searchParam = new SearchParam();
|
|
||||||
searchParam.setResourceName(resourceType);
|
|
||||||
searchParam.setParamName(paramName);
|
|
||||||
searchParam = mySearchParamDao.save(searchParam);
|
|
||||||
ourLog.info("Added search param {} with pid {}", paramName, searchParam.getId());
|
|
||||||
// Don't add the newly saved entity to the map in case the save fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchParamPresent present = new SearchParamPresent();
|
SearchParamPresent present = new SearchParamPresent();
|
||||||
present.setResource(theResource);
|
present.setResource(theResource);
|
||||||
present.setSearchParam(searchParam);
|
present.setParamName(paramName);
|
||||||
present.setPresent(next.getValue());
|
present.setPresent(next.getValue());
|
||||||
entitiesToSave.add(present);
|
present.calculateHashes();
|
||||||
|
|
||||||
|
newHashToPresence.put(present.getHashPresence(), present);
|
||||||
}
|
}
|
||||||
|
|
||||||
mySearchParamPresentDao.deleteInBatch(entitiesToDelete);
|
// Delete any that should be deleted
|
||||||
mySearchParamPresentDao.saveAll(entitiesToSave);
|
List<SearchParamPresent> toDelete = new ArrayList<>();
|
||||||
|
for (Entry<Long, SearchParamPresent> nextEntry : existingHashToPresence.entrySet()) {
|
||||||
|
if (newHashToPresence.containsKey(nextEntry.getKey()) == false) {
|
||||||
|
toDelete.add(nextEntry.getValue());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
mySearchParamPresentDao.deleteInBatch(toDelete);
|
||||||
|
|
||||||
|
// Add any that should be added
|
||||||
|
List<SearchParamPresent> toAdd = new ArrayList<>();
|
||||||
|
for (Entry<Long, SearchParamPresent> nextEntry : newHashToPresence.entrySet()) {
|
||||||
|
if (existingHashToPresence.containsKey(nextEntry.getKey()) == false) {
|
||||||
|
toAdd.add(nextEntry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mySearchParamPresentDao.saveAll(toAdd);
|
||||||
|
|
||||||
@Override
|
|
||||||
public void flushCachesForUnitTest() {
|
|
||||||
myResourceTypeToSearchParamToEntity.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -156,3 +156,8 @@ drop index IDX_FORCEDID_TYPE_FORCEDID;
|
|||||||
create index IDX_FORCEDID_TYPE_FID;
|
create index IDX_FORCEDID_TYPE_FID;
|
||||||
drop index IDX_SP_NUMBER;
|
drop index IDX_SP_NUMBER;
|
||||||
create index IDX_SP_NUMBER_HASH_VAL;
|
create index IDX_SP_NUMBER_HASH_VAL;
|
||||||
|
|
||||||
|
drop column SP_ID from table HFJ_RES_PARAM_PRESENT;
|
||||||
|
drop index IDX_SEARCHPARM_RESTYPE_SPNAME;
|
||||||
|
drop index IDX_RESPARMPRESENT_SPID_RESID;
|
||||||
|
drop table HFJ_SEARCH_PARM;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user