From 285872bf9f58c2420a2a93dcad9af9587e525fde Mon Sep 17 00:00:00 2001 From: srosse <none@none> Date: Mon, 11 Sep 2017 16:30:46 +0200 Subject: [PATCH] OO-3006: implement REST API to import, delete question in the question pool, add and remove authors to questions --- .../model/xml/AssessmentItemMetadata.java | 4 +- .../model/xml/ManifestMetadataBuilder.java | 40 +++ .../org/olat/modules/qpool/QPoolService.java | 2 +- .../modules/qpool/manager/CollectionDAO.java | 6 +- .../olat/modules/qpool/manager/PoolDAO.java | 4 +- .../qpool/manager/QuestionItemDAO.java | 6 +- .../manager/QuestionPoolServiceImpl.java | 2 +- .../modules/qpool/restapi/QuestionItemVO.java | 85 +++++ .../qpool/restapi/QuestionItemVOes.java | 64 ++++ .../qpool/restapi/QuestionPoolWebService.java | 298 ++++++++++++++++++ .../olat/restapi/_spring/restApiContext.xml | 1 + .../restapi/security/RestSecurityHelper.java | 9 + .../org/olat/restapi/QuestionPoolTest.java | 218 +++++++++++++ .../restapi/multiple_choice_per_answer.zip | Bin 0 -> 3079 bytes 14 files changed, 728 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/olat/modules/qpool/restapi/QuestionItemVO.java create mode 100644 src/main/java/org/olat/modules/qpool/restapi/QuestionItemVOes.java create mode 100644 src/main/java/org/olat/modules/qpool/restapi/QuestionPoolWebService.java create mode 100644 src/test/java/org/olat/restapi/QuestionPoolTest.java create mode 100644 src/test/java/org/olat/restapi/multiple_choice_per_answer.zip diff --git a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemMetadata.java b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemMetadata.java index 7f205a853c1..af88a7f3a71 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemMetadata.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemMetadata.java @@ -251,9 +251,11 @@ public class AssessmentItemMetadata { EducationalType educational = metadata.getEducational(false); if(educational != null) { - // + level = metadata.getEducationContext(); } + taxonomyPath = metadata.getClassificationTaxonomy(); + //qti metadata QTIMetadataType qtiMetadata = metadata.getQtiMetadata(true); if(qtiMetadata != null) { diff --git a/src/main/java/org/olat/ims/qti21/model/xml/ManifestMetadataBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/ManifestMetadataBuilder.java index 2d58b846626..01e478957b4 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/ManifestMetadataBuilder.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/ManifestMetadataBuilder.java @@ -26,6 +26,7 @@ import java.util.StringTokenizer; import javax.xml.bind.JAXBElement; import javax.xml.namespace.QName; +import org.olat.core.util.StringHelper; import org.olat.imscp.xml.manifest.ManifestMetadataType; import org.olat.imscp.xml.manifest.MetadataType; import org.olat.imsmd.xml.manifest.ClassificationType; @@ -204,6 +205,23 @@ public class ManifestMetadataBuilder { } } + public String getEducationContext() { + EducationalType educational = getEducational(true); + StringBuilder sb = new StringBuilder(); + if(educational != null) { + ContextType type = getFromAny(ContextType.class, educational.getContent()); + if(type != null && type.getSource() != null && type.getSource().getLangstring() != null + && type.getValue() != null && type.getValue().getLangstring() != null) { + String source = type.getSource().getLangstring().getValue(); + String value = type.getValue().getLangstring().getValue(); + if(StringHelper.containsNonWhitespace(source) && StringHelper.containsNonWhitespace(value)) { + sb.append(value); + } + } + } + return sb.length() == 0 ? null: sb.toString(); + } + public void setEducationalContext(String context, String lang) { EducationalType educational = getEducational(true); if(educational != null) { @@ -250,6 +268,28 @@ public class ManifestMetadataBuilder { } } + public String getClassificationTaxonomy() { + StringBuilder sb = new StringBuilder(); + ClassificationType classification = getClassification("discipline", null, false); + if(classification != null) { + TaxonpathType taxonpath = getFromAny(TaxonpathType.class, classification.getContent()); + if(taxonpath != null) { + List<TaxonType> taxons = taxonpath.getTaxon(); + if(taxons != null) { + for(TaxonType taxon:taxons) { + if(taxon.getEntry() != null && taxon.getEntry().getLangstring().size() > 0) { + LangstringType value = taxon.getEntry().getLangstring().get(0); + if(value != null && value.getValue() != null) { + sb.append("/").append(value.getValue()); + } + } + } + } + } + } + return sb.length() == 0 ? null : sb.toString(); + } + /** * Set a taxonomy path of purpose "discipline" * @param taxonomyPath diff --git a/src/main/java/org/olat/modules/qpool/QPoolService.java b/src/main/java/org/olat/modules/qpool/QPoolService.java index a7eb26affc3..24707c2d169 100644 --- a/src/main/java/org/olat/modules/qpool/QPoolService.java +++ b/src/main/java/org/olat/modules/qpool/QPoolService.java @@ -70,7 +70,7 @@ public interface QPoolService { public QuestionItem updateItem(QuestionItem item); - public void deleteItems(List<QuestionItemShort> items); + public void deleteItems(List<? extends QuestionItemShort> items); public int countItems(SearchQuestionItemParams params); diff --git a/src/main/java/org/olat/modules/qpool/manager/CollectionDAO.java b/src/main/java/org/olat/modules/qpool/manager/CollectionDAO.java index 7e1a300cf79..adea8da0a05 100644 --- a/src/main/java/org/olat/modules/qpool/manager/CollectionDAO.java +++ b/src/main/java/org/olat/modules/qpool/manager/CollectionDAO.java @@ -160,7 +160,7 @@ public class CollectionDAO { public int removeItemFromCollection(List<QuestionItemShort> items, QuestionItemCollection collection) { if(items == null || items.isEmpty()) return 0;//noting to do - List<Long> keys = new ArrayList<Long>(); + List<Long> keys = new ArrayList<>(); for(QuestionItemShort item:items) { keys.add(item.getKey()); } @@ -174,10 +174,10 @@ public class CollectionDAO { .executeUpdate(); } - public int deleteItemFromCollections(List<QuestionItemShort> items) { + public int deleteItemFromCollections(List<? extends QuestionItemShort> items) { if(items == null || items.isEmpty()) return 0;//noting to do - List<Long> keys = new ArrayList<Long>(); + List<Long> keys = new ArrayList<>(); for(QuestionItemShort item:items) { keys.add(item.getKey()); } diff --git a/src/main/java/org/olat/modules/qpool/manager/PoolDAO.java b/src/main/java/org/olat/modules/qpool/manager/PoolDAO.java index 4b6b890e4cf..e373e31b992 100644 --- a/src/main/java/org/olat/modules/qpool/manager/PoolDAO.java +++ b/src/main/java/org/olat/modules/qpool/manager/PoolDAO.java @@ -73,10 +73,10 @@ public class PoolDAO { return pool; } - public int removeFromPools(List<QuestionItemShort> items) { + public int removeFromPools(List<? extends QuestionItemShort> items) { if(items == null || items.isEmpty()) return 0; - List<Long> keys = new ArrayList<Long>(); + List<Long> keys = new ArrayList<>(); for(QuestionItemShort item:items) { keys.add(item.getKey()); } diff --git a/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java b/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java index 6cb7c42604f..e94021a0bde 100644 --- a/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java +++ b/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java @@ -423,8 +423,8 @@ public class QuestionItemDAO { .getResultList(); } - public int removeFromShares(List<QuestionItemShort> items) { - List<Long> keys = new ArrayList<Long>(); + public int removeFromShares(List<? extends QuestionItemShort> items) { + List<Long> keys = new ArrayList<>(); for(QuestionItemShort item:items) { keys.add(item.getKey()); } @@ -437,7 +437,7 @@ public class QuestionItemDAO { } public int removeFromShare(List<QuestionItemShort> items, OLATResource resource) { - List<Long> keys = new ArrayList<Long>(); + List<Long> keys = new ArrayList<>(); for(QuestionItemShort item:items) { keys.add(item.getKey()); } diff --git a/src/main/java/org/olat/modules/qpool/manager/QuestionPoolServiceImpl.java b/src/main/java/org/olat/modules/qpool/manager/QuestionPoolServiceImpl.java index d451ec13074..bae904c8fa6 100644 --- a/src/main/java/org/olat/modules/qpool/manager/QuestionPoolServiceImpl.java +++ b/src/main/java/org/olat/modules/qpool/manager/QuestionPoolServiceImpl.java @@ -114,7 +114,7 @@ public class QuestionPoolServiceImpl implements QPoolService { @Override - public void deleteItems(List<QuestionItemShort> items) { + public void deleteItems(List<? extends QuestionItemShort> items) { if(items == null || items.isEmpty()) { return; //nothing to do } diff --git a/src/main/java/org/olat/modules/qpool/restapi/QuestionItemVO.java b/src/main/java/org/olat/modules/qpool/restapi/QuestionItemVO.java new file mode 100644 index 00000000000..3827ead6026 --- /dev/null +++ b/src/main/java/org/olat/modules/qpool/restapi/QuestionItemVO.java @@ -0,0 +1,85 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.modules.qpool.restapi; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +import org.olat.modules.qpool.QuestionItem; + +/** + * + * Initial date: 8 sept. 2017<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "questionItemVO") +public class QuestionItemVO { + + private Long key; + private String identifier; + private String masterIdentifier; + private String title; + + public QuestionItemVO() { + // + } + + public QuestionItemVO(QuestionItem item) { + key = item.getKey(); + identifier = item.getIdentifier(); + masterIdentifier = item.getMasterIdentifier(); + title = item.getTitle(); + } + + public Long getKey() { + return key; + } + + public void setKey(Long key) { + this.key = key; + } + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public String getMasterIdentifier() { + return masterIdentifier; + } + + public void setMasterIdentifier(String masterIdentifier) { + this.masterIdentifier = masterIdentifier; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } +} diff --git a/src/main/java/org/olat/modules/qpool/restapi/QuestionItemVOes.java b/src/main/java/org/olat/modules/qpool/restapi/QuestionItemVOes.java new file mode 100644 index 00000000000..6be10a576a7 --- /dev/null +++ b/src/main/java/org/olat/modules/qpool/restapi/QuestionItemVOes.java @@ -0,0 +1,64 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.modules.qpool.restapi; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * + * Initial date: 8 sept. 2017<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "questionItemVOes") +public class QuestionItemVOes { + + @XmlElementWrapper(name="questionItems") + @XmlElement(name="questionItem") + private QuestionItemVO[] questionItems; + @XmlAttribute(name="totalCount") + private int totalCount; + + public QuestionItemVOes() { + // + } + + public QuestionItemVO[] getQuestionItems() { + return questionItems; + } + + public void setQuestionItems(QuestionItemVO[] questionItems) { + this.questionItems = questionItems; + } + + public int getTotalCount() { + return totalCount; + } + + public void setTotalCount(int totalCount) { + this.totalCount = totalCount; + } +} diff --git a/src/main/java/org/olat/modules/qpool/restapi/QuestionPoolWebService.java b/src/main/java/org/olat/modules/qpool/restapi/QuestionPoolWebService.java new file mode 100644 index 00000000000..60323d02866 --- /dev/null +++ b/src/main/java/org/olat/modules/qpool/restapi/QuestionPoolWebService.java @@ -0,0 +1,298 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.modules.qpool.restapi; + +import static org.olat.restapi.security.RestSecurityHelper.isQuestionPoolManager; + +import java.io.File; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.olat.basesecurity.BaseSecurity; +import org.olat.core.CoreSpringFactory; +import org.olat.core.id.Identity; +import org.olat.core.logging.OLog; +import org.olat.core.logging.Tracing; +import org.olat.core.util.i18n.I18nManager; +import org.olat.modules.qpool.QPoolService; +import org.olat.modules.qpool.QuestionItem; +import org.olat.modules.qpool.QuestionItemShort; +import org.olat.restapi.security.RestSecurityHelper; +import org.olat.restapi.support.MultipartReader; +import org.olat.user.restapi.UserVO; +import org.olat.user.restapi.UserVOFactory; + +/** + * + * Initial date: 8 sept. 2017<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +@Path("qpool/items") +public class QuestionPoolWebService { + + private static final OLog log = Tracing.createLoggerFor(QuestionPoolWebService.class); + + @PUT + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + @Consumes({MediaType.MULTIPART_FORM_DATA}) + public Response importQuestionItemsPut(@Context HttpServletRequest request) { + return importQuestionItems(request); + } + + @POST + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + @Consumes({MediaType.MULTIPART_FORM_DATA}) + public Response importQuestionItemsPost(@Context HttpServletRequest request) { + return importQuestionItems(request); + } + + private Response importQuestionItems(HttpServletRequest request) { + if(!isQuestionPoolManager(request)) { + return Response.serverError().status(Status.UNAUTHORIZED).build(); + } + + MultipartReader partsReader = null; + try { + Identity identity = RestSecurityHelper.getUserRequest(request).getIdentity(); + partsReader = new MultipartReader(request); + File tmpFile = partsReader.getFile(); + long length = tmpFile.length(); + if(length > 0) { + String filename = partsReader.getValue("filename"); + String language = partsReader.getValue("language"); + QuestionItemVOes voes = importQuestionItem(identity, filename, tmpFile, language); + return Response.ok(voes).build(); + } + return Response.serverError().status(Status.NO_CONTENT).build(); + } catch (Exception e) { + log.error("Error while importing a file",e); + } finally { + MultipartReader.closeQuietly(partsReader); + } + return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); + } + + private QuestionItemVOes importQuestionItem(Identity owner, String filename, File tmpFile, String language) { + Locale locale = CoreSpringFactory.getImpl(I18nManager.class).getLocaleOrDefault(language); + + List<QuestionItem> items = CoreSpringFactory.getImpl(QPoolService.class) + .importItems(owner, locale, filename, tmpFile); + QuestionItemVOes voes = new QuestionItemVOes(); + QuestionItemVO[] voArray = new QuestionItemVO[items.size()]; + for(int i=items.size(); i-->0; ) { + voArray[i] = new QuestionItemVO(items.get(i)); + } + voes.setQuestionItems(voArray); + voes.setTotalCount(items.size()); + return voes; + } + + /** + * Delete a question item by id. + * + * @response.representation.200.doc Nothing + * @response.representation.401.doc The roles of the authenticated user are not sufficient + * @response.representation.404.doc The question item not found + * @param itemKey The question item identifier + * @param request The HTTP request + * @return Nothing + */ + @DELETE + @Path("{itemKey}") + public Response deleteQuestionItem(@PathParam("itemKey") Long itemKey, @Context HttpServletRequest request) { + if(!isQuestionPoolManager(request)) { + return Response.serverError().status(Status.UNAUTHORIZED).build(); + } + QPoolService poolService = CoreSpringFactory.getImpl(QPoolService.class); + QuestionItem item = poolService.loadItemById(itemKey); + if(item == null) { + return Response.serverError().status(Status.NOT_FOUND).build(); + } + List<QuestionItem> itemToDelete = Collections.singletonList(item); + poolService.deleteItems(itemToDelete); + + return Response.ok().build(); + } + + /** + * Get all authors of the question item. + * + * @response.representation.200.qname {http://www.example.com}userVO + * @response.representation.200.mediaType application/xml, application/json + * @response.representation.200.doc The array of authors + * @response.representation.401.doc The roles of the authenticated user are not sufficient + * @response.representation.404.doc The question item not found + * @param itemKey The question item identifier + * @param httpRequest The HTTP request + * @return It returns an array of <code>UserVO</code> + */ + @GET + @Path("{itemKey}/authors") + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + public Response getAuthors(@PathParam("itemKey") Long itemKey, + @Context HttpServletRequest request) { + + if(!isQuestionPoolManager(request)) { + return Response.serverError().status(Status.UNAUTHORIZED).build(); + } + + QPoolService poolService = CoreSpringFactory.getImpl(QPoolService.class); + QuestionItem item = poolService.loadItemById(itemKey); + if(item == null) { + return Response.serverError().status(Status.NOT_FOUND).build(); + } + List<Identity> authorList = poolService.getAuthors(item); + + int count = 0; + UserVO[] authors = new UserVO[authorList.size()]; + for(Identity author:authorList) { + authors[count++] = UserVOFactory.get(author); + } + return Response.ok(authors).build(); + } + + /** + * Get this specific author of the quesiton item. + * + * @response.representation.200.qname {http://www.example.com}userVO + * @response.representation.200.mediaType application/xml, application/json + * @response.representation.200.doc The author + * @response.representation.401.doc The roles of the authenticated user are not sufficient + * @response.representation.404.doc The question item not found or the user is not an author of the course + * @param itemKey The question item identifier + * @param identityKey The user identifier + * @param httpRequest The HTTP request + * @return It returns an <code>UserVO</code> + */ + @GET + @Path("{itemKey}/authors/{identityKey}") + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + public Response getAuthor(@PathParam("itemKey") Long itemKey, @PathParam("identityKey") Long identityKey, + @Context HttpServletRequest request) { + if(!isQuestionPoolManager(request)) { + return Response.serverError().status(Status.UNAUTHORIZED).build(); + } + + QPoolService poolService = CoreSpringFactory.getImpl(QPoolService.class); + QuestionItem item = poolService.loadItemById(itemKey); + if(item == null) { + return Response.serverError().status(Status.NOT_FOUND).build(); + } + List<Identity> authorList = poolService.getAuthors(item); + for(Identity author:authorList) { + if(author.getKey().equals(identityKey)) { + UserVO authorVo = UserVOFactory.get(author); + return Response.ok(authorVo).build(); + } + } + return Response.serverError().status(Status.NOT_FOUND).build(); + } + + /** + * Add an author to the question item. + * + * @response.representation.200.doc The user is an author of the question item + * @response.representation.401.doc The roles of the authenticated user are not sufficient + * @response.representation.404.doc The question item or the user not found + * @param itemKey The question item identifier + * @param identityKey The user identifier + * @param httpRequest The HTTP request + * @return It returns 200 if the user is added as author of the question item + */ + @PUT + @Path("{itemKey}/authors/{identityKey}") + public Response addAuthor(@PathParam("itemKey") Long itemKey, @PathParam("identityKey") Long identityKey, + @Context HttpServletRequest httpRequest) { + if(!isQuestionPoolManager(httpRequest)) { + return Response.serverError().status(Status.UNAUTHORIZED).build(); + } + + QPoolService poolService = CoreSpringFactory.getImpl(QPoolService.class); + QuestionItem item = poolService.loadItemById(itemKey); + if(item == null) { + return Response.serverError().status(Status.NOT_FOUND).build(); + } + + BaseSecurity securityManager = CoreSpringFactory.getImpl(BaseSecurity.class); + Identity author = securityManager.loadIdentityByKey(identityKey, false); + if(author == null) { + return Response.serverError().status(Status.NOT_FOUND).build(); + } + + List<Identity> authors = Collections.singletonList(author); + List<QuestionItemShort> items = Collections.singletonList(item); + poolService.addAuthors(authors, items); + return Response.ok().build(); + } + + /** + * Remove an author to the question item. + * + * @response.representation.200.doc The user was successfully removed as author of the question item + * @response.representation.401.doc The roles of the authenticated user are not sufficient + * @response.representation.404.doc The question item or the user not found + * @param itemKey The question item identifier + * @param identityKey The user identifier + * @param httpRequest The HTTP request + * @return It returns 200 if the user is removed as author of the question item + */ + @DELETE + @Path("{itemKey}/authors/{identityKey}") + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + public Response removeAuthor(@PathParam("itemKey") Long itemKey, @PathParam("identityKey") Long identityKey, + @Context HttpServletRequest httpRequest) { + if(!isQuestionPoolManager(httpRequest)) { + return Response.serverError().status(Status.UNAUTHORIZED).build(); + } + + QPoolService poolService = CoreSpringFactory.getImpl(QPoolService.class); + QuestionItem item = poolService.loadItemById(itemKey); + if(item == null) { + return Response.serverError().status(Status.NOT_FOUND).build(); + } + + BaseSecurity securityManager = CoreSpringFactory.getImpl(BaseSecurity.class); + Identity author = securityManager.loadIdentityByKey(identityKey, false); + if(author == null) { + return Response.serverError().status(Status.NOT_FOUND).build(); + } + + List<Identity> authors = Collections.singletonList(author); + List<QuestionItemShort> items = Collections.singletonList(item); + poolService.removeAuthors(authors, items); + return Response.ok().build(); + } +} diff --git a/src/main/java/org/olat/restapi/_spring/restApiContext.xml b/src/main/java/org/olat/restapi/_spring/restApiContext.xml index 74745c58a9b..abd6ba571dd 100644 --- a/src/main/java/org/olat/restapi/_spring/restApiContext.xml +++ b/src/main/java/org/olat/restapi/_spring/restApiContext.xml @@ -35,6 +35,7 @@ <value>org.olat.course.nodes.bc.BCWebService</value> <value>org.olat.course.assessment.restapi.EfficiencyStatementWebService</value> <value>org.olat.course.certificate.restapi.CertificationWebService</value> + <value>org.olat.modules.qpool.restapi.QuestionPoolWebService</value> <value>org.olat.modules.wiki.restapi.WikisWebService</value> <value>org.olat.modules.fo.restapi.ForumImportWebService</value> <value>org.olat.modules.fo.restapi.ForumCourseNodeWebService</value> diff --git a/src/main/java/org/olat/restapi/security/RestSecurityHelper.java b/src/main/java/org/olat/restapi/security/RestSecurityHelper.java index e347e34aaba..6de4d7ae762 100644 --- a/src/main/java/org/olat/restapi/security/RestSecurityHelper.java +++ b/src/main/java/org/olat/restapi/security/RestSecurityHelper.java @@ -145,6 +145,15 @@ public class RestSecurityHelper { } } + public static boolean isQuestionPoolManager(HttpServletRequest request) { + try { + Roles roles = getRoles(request); + return (roles.isPoolAdmin() || roles.isOLATAdmin()); + } catch (Exception e) { + return false; + } + } + public static boolean isAdmin(HttpServletRequest request) { try { Roles roles = getRoles(request); diff --git a/src/test/java/org/olat/restapi/QuestionPoolTest.java b/src/test/java/org/olat/restapi/QuestionPoolTest.java new file mode 100644 index 00000000000..db251f851ff --- /dev/null +++ b/src/test/java/org/olat/restapi/QuestionPoolTest.java @@ -0,0 +1,218 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.restapi; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriBuilder; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.util.EntityUtils; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.type.TypeReference; +import org.junit.Assert; +import org.junit.Test; +import org.olat.core.commons.persistence.DB; +import org.olat.core.id.Identity; +import org.olat.ims.qti.QTIConstants; +import org.olat.modules.qpool.QPoolService; +import org.olat.modules.qpool.QuestionItem; +import org.olat.modules.qpool.QuestionType; +import org.olat.modules.qpool.manager.QItemTypeDAO; +import org.olat.modules.qpool.manager.QuestionItemDAO; +import org.olat.modules.qpool.model.QItemType; +import org.olat.modules.qpool.restapi.QuestionItemVO; +import org.olat.modules.qpool.restapi.QuestionItemVOes; +import org.olat.test.JunitTestHelper; +import org.olat.test.OlatJerseyTestCase; +import org.olat.user.restapi.UserVO; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * + * Initial date: 8 sept. 2017<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class QuestionPoolTest extends OlatJerseyTestCase { + + @Autowired + private DB dbInstance; + @Autowired + private QPoolService qpoolService; + @Autowired + private QItemTypeDAO qItemTypeDao; + @Autowired + private QuestionItemDAO questionDao; + @Autowired + private QuestionItemDAO questionItemDao; + + @Test + public void importQuestion() + throws IOException, URISyntaxException { + URL itemUrl = QuestionPoolTest.class.getResource("multiple_choice_per_answer.zip"); + assertNotNull(itemUrl); + File itemFile = new File(itemUrl.toURI()); + + RestConnection conn = new RestConnection(); + assertTrue(conn.login("administrator", "openolat")); + + URI request = UriBuilder.fromUri(getContextURI()).path("qpool/items").build(); + HttpPut method = conn.createPut(request, MediaType.APPLICATION_JSON, true); + HttpEntity entity = MultipartEntityBuilder.create() + .setMode(HttpMultipartMode.BROWSER_COMPATIBLE) + .addBinaryBody("file", itemFile, ContentType.APPLICATION_OCTET_STREAM, itemFile.getName()) + .addTextBody("filename", "multiple_choice_per_answer.zip") + .build(); + method.setEntity(entity); + + HttpResponse response = conn.execute(method); + Assert.assertEquals(200, response.getStatusLine().getStatusCode()); + + QuestionItemVOes voes = conn.parse(response, QuestionItemVOes.class); + Assert.assertNotNull(voes); + QuestionItemVO[] voArray = voes.getQuestionItems(); + Assert.assertNotNull(voArray); + Assert.assertEquals(1, voArray.length); + QuestionItemVO vo = voArray[0]; + Assert.assertNotNull(vo); + } + + @Test + public void getAuthors() throws IOException, URISyntaxException { + QItemType mcType = qItemTypeDao.loadByType(QuestionType.MC.name()); + Identity author = JunitTestHelper.createAndPersistIdentityAsRndUser("item-author-1"); + QuestionItem item = questionDao.createAndPersist(author, "NGC 55", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, null, mcType); + dbInstance.commitAndCloseSession(); + + RestConnection conn = new RestConnection(); + Assert.assertTrue(conn.login("administrator", "openolat")); + + URI request = UriBuilder.fromUri(getContextURI()).path("/qpool/items/" + item.getKey() + "/authors/").build(); + HttpGet method = conn.createGet(request, MediaType.APPLICATION_JSON, true); + HttpResponse response = conn.execute(method); + Assert.assertEquals(200, response.getStatusLine().getStatusCode()); + List<UserVO> users = parseUserArray(response.getEntity().getContent()); + //check + Assert.assertNotNull(users); + Assert.assertEquals(1, users.size()); + Assert.assertTrue(author.getKey().equals(users.get(0).getKey())); + } + + @Test + public void getAuthor() throws IOException, URISyntaxException { + QItemType mcType = qItemTypeDao.loadByType(QuestionType.MC.name()); + Identity author = JunitTestHelper.createAndPersistIdentityAsRndUser("item-author-2"); + QuestionItem item = questionDao.createAndPersist(author, "NGC 55", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, null, mcType); + dbInstance.commitAndCloseSession(); + + RestConnection conn = new RestConnection(); + Assert.assertTrue(conn.login("administrator", "openolat")); + + URI request = UriBuilder.fromUri(getContextURI()).path("/qpool/items/" + item.getKey() + "/authors/" + author.getKey()).build(); + HttpGet method = conn.createGet(request, MediaType.APPLICATION_JSON, true); + HttpResponse response = conn.execute(method); + Assert.assertEquals(200, response.getStatusLine().getStatusCode()); + UserVO user = conn.parse(response.getEntity().getContent(), UserVO.class); + //check + Assert.assertNotNull(user); + Assert.assertTrue(author.getKey().equals(user.getKey())); + } + + @Test + public void addAuthor() throws IOException, URISyntaxException { + QItemType mcType = qItemTypeDao.loadByType(QuestionType.MC.name()); + Identity author = JunitTestHelper.createAndPersistIdentityAsRndUser("item-author-1"); + QuestionItem item = questionDao.createAndPersist(author, "NGC 55", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, null, mcType); + Identity coAuthor = JunitTestHelper.createAndPersistIdentityAsRndUser("item-author-1"); + + RestConnection conn = new RestConnection(); + Assert.assertTrue(conn.login("administrator", "openolat")); + + URI request = UriBuilder.fromUri(getContextURI()).path("/qpool/items/" + item.getKey() + "/authors/" + coAuthor.getKey()).build(); + HttpPut method = conn.createPut(request, MediaType.APPLICATION_JSON, true); + HttpResponse response = conn.execute(method); + Assert.assertEquals(200, response.getStatusLine().getStatusCode()); + EntityUtils.consume(response.getEntity()); + + //check + List<Identity> authors = qpoolService.getAuthors(item); + Assert.assertNotNull(authors); + Assert.assertEquals(2, authors.size()); + Assert.assertTrue(authors.contains(author)); + Assert.assertTrue(authors.contains(coAuthor)); + } + + @Test + public void removeAuthor() throws IOException, URISyntaxException { + QItemType mcType = qItemTypeDao.loadByType(QuestionType.MC.name()); + Identity author = JunitTestHelper.createAndPersistIdentityAsRndUser("item-author-1"); + QuestionItem item = questionDao.createAndPersist(author, "NGC 55", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, null, mcType); + Identity coAuthor = JunitTestHelper.createAndPersistIdentityAsRndUser("item-author-1"); + List<Identity> authors = Collections.singletonList(coAuthor); + questionItemDao.addAuthors(authors, item); + dbInstance.commit(); + + RestConnection conn = new RestConnection(); + Assert.assertTrue(conn.login("administrator", "openolat")); + + URI request = UriBuilder.fromUri(getContextURI()).path("/qpool/items/" + item.getKey() + "/authors/" + coAuthor.getKey()).build(); + HttpDelete method = conn.createDelete(request, MediaType.APPLICATION_JSON); + HttpResponse response = conn.execute(method); + Assert.assertEquals(200, response.getStatusLine().getStatusCode()); + EntityUtils.consume(response.getEntity()); + + //check + List<Identity> itemsAuthors = qpoolService.getAuthors(item); + Assert.assertNotNull(itemsAuthors); + Assert.assertEquals(1, itemsAuthors.size()); + Assert.assertTrue(itemsAuthors.contains(author)); + Assert.assertFalse(itemsAuthors.contains(coAuthor)); + } + + protected List<UserVO> parseUserArray(InputStream body) { + try { + ObjectMapper mapper = new ObjectMapper(jsonFactory); + return mapper.readValue(body, new TypeReference<List<UserVO>>(){/* */}); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/src/test/java/org/olat/restapi/multiple_choice_per_answer.zip b/src/test/java/org/olat/restapi/multiple_choice_per_answer.zip new file mode 100644 index 0000000000000000000000000000000000000000..db49796f9efeb569f151a9a05cf932505e473a30 GIT binary patch literal 3079 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 Jv(Yb&{u$1Q{dxcZ literal 0 HcmV?d00001 -- GitLab