diff --git a/.hgtags b/.hgtags index 1bdc1077f51183ad0dc18e8f31d2d523b8a3b78a..8b7ba699727b50a89c449d009341cb33b491ad39 100644 --- a/.hgtags +++ b/.hgtags @@ -191,3 +191,5 @@ dda2b6a8c3454872516fac37667698425802fc97 OpenOLAT 11.5.1 0e8d6b8d69a92131e4b38e9117e09fb297eef38e OpenOLAT 11.5.3 614f0f09f5a7a915a3a364e3f5641e9179dc7735 OpenOLAT 12.0.0 03f92c40a74397b179cce2b4b2d3346c29a20e64 OpenOLAT 12.0.1 +9266c599d0a13634e44724540a37fb4161e29f98 OpenOLAT 11.5.4 +15761fa18b4b2eebe3e4592e4289527989c7f256 OpenOLAT 12.0.2 diff --git a/src/main/java/org/olat/course/nodes/gta/manager/GTANotifications.java b/src/main/java/org/olat/course/nodes/gta/manager/GTANotifications.java index d40f9536f44e9a4483220a0bbc38c411f893e319..e3d5d7db0d41e19cfbb386c87e54d501c7c19722 100644 --- a/src/main/java/org/olat/course/nodes/gta/manager/GTANotifications.java +++ b/src/main/java/org/olat/course/nodes/gta/manager/GTANotifications.java @@ -200,7 +200,7 @@ class GTANotifications { } else { Task task = gtaManager.getTask(subscriberIdentity, taskList); if(task != null) { - header = translator.translate("notifications.individual.header.task", new String[]{ task.getTaskName(), displayName }); + header = translator.translate("notifications.individual.header.task", new String[]{ getTaskName(task), displayName }); } } @@ -231,7 +231,7 @@ class GTANotifications { File[] submissions = submitDirectory.listFiles(SystemFileFilter.FILES_ONLY); if(submissions.length == 0) { String[] params = new String[] { - task.getTaskName(), // {0} + getTaskName(task), // {0} displayName, // {1} fullName // {2} }; @@ -239,7 +239,7 @@ class GTANotifications { } else { for(File submission:submissions) { String[] params = new String[] { - task.getTaskName(), // {0} + getTaskName(task), // {0} displayName, // {1} submission.getName(), // {2} fullName // {3} @@ -279,7 +279,7 @@ class GTANotifications { if(groups.size() == 1 && !owner && !membership.isCoach()) { Task task = gtaManager.getTask(groups.get(0), taskList); if(task != null) { - header = translator.translate("notifications.group.header.task", new String[]{ task.getTaskName(), displayName }); + header = translator.translate("notifications.group.header.task", new String[]{ getTaskName(task), displayName }); } } @@ -307,7 +307,7 @@ class GTANotifications { File[] submisssions = submitDirectory.listFiles(SystemFileFilter.FILES_ONLY); if(submisssions.length == 0) { String[] params = new String[] { - task.getTaskName(), + getTaskName(task), displayName, group.getName() }; @@ -317,7 +317,7 @@ class GTANotifications { for(File submission:submisssions) { String author = getAuthor(submission, submitContainer); String[] params = new String[] { - task.getTaskName(), // {0} + getTaskName(task), // {0} displayName, // {1} submission.getName(), // {2} author, // {3} @@ -364,14 +364,14 @@ class GTANotifications { if(sendNotificationDueDate) { if(task.getRevisionsDueDate() != null) { String[] params = new String[] { - task.getTaskName(), + getTaskName(task), displayName, formatter.formatDateAndTime(task.getRevisionsDueDate()) }; appendSubscriptionItem("notifications.correction.duedate", params, assessedIdentity, correctionDate, coach); } else { String[] params = new String[] { - task.getTaskName(), + getTaskName(task), displayName }; appendSubscriptionItem("notifications.correction", params, assessedIdentity, correctionDate, coach); @@ -383,7 +383,7 @@ class GTANotifications { for(File correction:corrections) { String author = getAuthor(correction, correctionContainer); String[] params = new String[] { - task.getTaskName(), + getTaskName(task), displayName, correction.getName(), author @@ -417,7 +417,7 @@ class GTANotifications { File[] revisions = revisionDirectory.listFiles(SystemFileFilter.FILES_ONLY); if(revisions.length == 0) { String[] params = new String[] { - task.getTaskName(), + getTaskName(task), displayName, name }; @@ -430,7 +430,7 @@ class GTANotifications { for(File revision:revisions) { String author = getAuthor(revision, revisionContainer); String[] params = new String[] { - task.getTaskName(), + getTaskName(task), displayName, revision.getName(), name, @@ -462,14 +462,14 @@ class GTANotifications { if(sendNotificationDueDate) { if(task.getRevisionsDueDate() != null) { String[] params = new String[] { - task.getTaskName(), + getTaskName(task), displayName, formatter.formatDateAndTime(task.getRevisionsDueDate()) }; appendSubscriptionItem("notifications.correction.duedate", params, assessedIdentity, correctionDate, coach); } else { String[] params = new String[] { - task.getTaskName(), + getTaskName(task), displayName }; appendSubscriptionItem("notifications.correction", params, assessedIdentity, correctionDate, coach); @@ -481,7 +481,7 @@ class GTANotifications { for(File correction:corrections) { String author = getAuthor(correction, correctionContainer); String[] params = new String[] { - task.getTaskName(), + getTaskName(task), displayName, correction.getName(), author @@ -501,7 +501,7 @@ class GTANotifications { if(task.getAcceptationDate().after(compareDate)) { RepositoryEntry courseEntry = courseEnv.getCourseGroupManager().getCourseEntry(); String[] params = new String[] { - task.getTaskName(), + getTaskName(task), courseEntry.getDisplayname() }; if(assessedGroup != null) { @@ -533,7 +533,7 @@ class GTANotifications { Date graduationDate = task.getGraduationDate(); String[] params = new String[] { - task.getTaskName(), + getTaskName(task), courseEntry.getDisplayname(), score, status @@ -557,7 +557,7 @@ class GTANotifications { List<File> docs = gtaNode.getIndividualAssessmentDocuments(assessedUserCourseEnv); for(File doc:docs) { String[] docParams = new String[] { - task.getTaskName(), + getTaskName(task), courseEntry.getDisplayname(), doc.getName() }; @@ -637,7 +637,7 @@ class GTANotifications { String author = getAuthor(solution, solutionContainer); if(task != null) { String[] params = new String[] { - task.getTaskName(), + getTaskName(task), displayName, solution.getName(), author @@ -868,4 +868,12 @@ class GTANotifications { } return ok; } + + private String getTaskName (Task task) { + if (!StringHelper.containsNonWhitespace(task.getTaskName())) { + return gtaNode.getShortTitle(); + } else { + return task.getTaskName(); + } + } } 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 7f205a853c107703428109e77c0960dc6ec46f2c..af88a7f3a713f806f4706612dafbe8a537e64d00 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 2d58b8466267d7979998a708869f533db3851326..01e478957b407ea7132a4a4c8fdd5f9d4ecd4599 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/ims/qti21/ui/_i18n/LocalStrings_fr.properties b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_fr.properties index 3f285363b48309b9ee3fe12cc1770300237f1f63..96331157e6abf2e505c27b8f4de743f4d2e3c3e5 100644 --- a/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_fr.properties +++ b/src/main/java/org/olat/ims/qti21/ui/_i18n/LocalStrings_fr.properties @@ -147,7 +147,7 @@ qti.form.results.onfinish=$org.olat.course.nodes.iq\:qti.form.results.onfinish qti.form.scoreprogress=$org.olat.course.nodes.iq\:qti.form.scoreprogress qti.form.setting.choose=Choisir un profile... qti.form.setting.formative=Formatif (exercice) -qti.form.setting.summative=Normatif (test r\u00E9el) +qti.form.setting.summative=Somatif (test r\u00E9el) qti.form.showfeedbacks=Montrer le retour d'informations qti.form.summary=$org.olat.course.nodes.iq\:qti.form.summary qti.form.summary.help=$org.olat.course.nodes.iq\:qti.form.summary.help diff --git a/src/main/java/org/olat/modules/qpool/QPoolService.java b/src/main/java/org/olat/modules/qpool/QPoolService.java index a7eb26affc39f1253978549f15d6a4c4e0989afe..24707c2d169a6d02919cfd774bdc11fdfa55a879 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 7e1a300cf7949286c9f94d1c9f8da08ce907b91e..adea8da0a05d7b9100360f1486869b7980643d6b 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 4b6b890e4cfcb044f47e20fab70b5ca3ce53d68c..e373e31b992e7a409703c87a588cdefc57b55b50 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 6cb7c42604f02485a18f907a0c025b0a75e46dd4..e94021a0bde370ae650c14c4e5909b748b08ec6d 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 d451ec13074dd9bef6b2464a5b0e85356ca0ab89..bae904c8fa6e7cb63205acd9be1f364128b9eea3 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 0000000000000000000000000000000000000000..3827ead6026a7f976bf804f2f6af8031c89f9384 --- /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 0000000000000000000000000000000000000000..6be10a576a7214e06f258b1430d1e60e87341964 --- /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 0000000000000000000000000000000000000000..60323d0286674345451f9f4a9d8792355f88e45f --- /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 e739992d80a6e4ddd675298e11a451421adf3309..9addf2bf980d20b7563b14638fac0020c5921b4e 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 e347e34aaba7c09073027859fdbbe26e559a0085..6de4d7ae762bb19375e619523542b760b67a119f 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 0000000000000000000000000000000000000000..db251f851ffb51a4eb9ea9904cb749504718a9f6 --- /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 Binary files /dev/null and b/src/test/java/org/olat/restapi/multiple_choice_per_answer.zip differ