Index: lams_central/build.properties =================================================================== diff -u -r7475d08afc280b5e2e5ddf04e8bf35e3166aaf80 -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/build.properties (.../build.properties) (revision 7475d08afc280b5e2e5ddf04e8bf35e3166aaf80) +++ lams_central/build.properties (.../build.properties) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -5,4 +5,9 @@ product.path.filesystem=org/lamsfoundation/lams/central # Project is a core one, not a Tool -product.core=true \ No newline at end of file +product.core=true + +# Control which activities are generated for TBL templates - vanilla setting is "checked" (not in quotes) +template.tbl.show.introduction= +template.tbl.show.preview=checked +template.tbl.show.notebook= Index: lams_central/build.xml =================================================================== diff -u -r62aaf160878735888d077bf28fac3c1989bb8fbd -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/build.xml (.../build.xml) (revision 62aaf160878735888d077bf28fac3c1989bb8fbd) +++ lams_central/build.xml (.../build.xml) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -34,6 +34,32 @@ + + + ${ant.project.name}: Copying JSP files + + + Copying web resources + + + + + + + + + Configuring template files to generate desired activities + + + + + + + + + + + Index: lams_central/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -r427b1699f540858bcf2d07288f0f5be431e8881b -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 427b1699f540858bcf2d07288f0f5be431e8881b) +++ lams_central/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -878,6 +878,6 @@ authoring.tbl.template.description =Individual and Team Readiness Assessments followed by Application Exercises. authoring.tbl.desc.question =These questions are for iRA and tRA. Click "Create Question" to add more questions. Turn on "Enable confidence levels" to have the learners' confidence levels from the iRA shown on the tRA screen. authoring.tbl.desc.ae =State the questions for AE. Click "Create Question" to add more questions. +authoring.template.basic.import.qti =Import IMS QTI - #======= End labels: Exported 872 labels for en AU ===== Index: lams_central/conf/language/lams/TBLResources.properties =================================================================== diff -u -r7475d08afc280b5e2e5ddf04e8bf35e3166aaf80 -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/conf/language/lams/TBLResources.properties (.../TBLResources.properties) (revision 7475d08afc280b5e2e5ddf04e8bf35e3166aaf80) +++ lams_central/conf/language/lams/TBLResources.properties (.../TBLResources.properties) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -14,4 +14,7 @@ boilerplate.individual.reflection.title=Notebook boilerplate.individual.reflection.instructions=What are your key learning points? boilerplate.peerreview=Peer Review -boilerplate.peerreview.instructions=Please rate your peers' interaction with the group. \ No newline at end of file +boilerplate.peerreview.instructions=Please rate your peers' interaction with the group. +boilerplate.before.ira.gate=Gate for iRA Test +boilerplate.before.tra.gate=Gate for tRA Test +boilerplate.before.app.ex=Gate for Application Exercise(s) \ No newline at end of file Index: lams_central/src/java/org/lamsfoundation/lams/authoring/template/web/LdTemplateController.java =================================================================== diff -u -rba30ff8870a09d3979846f37d807a65876717dfc -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/src/java/org/lamsfoundation/lams/authoring/template/web/LdTemplateController.java (.../LdTemplateController.java) (revision ba30ff8870a09d3979846f37d807a65876717dfc) +++ lams_central/src/java/org/lamsfoundation/lams/authoring/template/web/LdTemplateController.java (.../LdTemplateController.java) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -24,6 +24,7 @@ import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.text.MessageFormat; import java.util.LinkedList; import java.util.ResourceBundle; @@ -44,6 +45,9 @@ import org.lamsfoundation.lams.learningdesign.Grouping; import org.lamsfoundation.lams.learningdesign.LearningDesign; import org.lamsfoundation.lams.learningdesign.exception.LearningDesignException; +import org.lamsfoundation.lams.questions.Answer; +import org.lamsfoundation.lams.questions.Question; +import org.lamsfoundation.lams.questions.QuestionParser; import org.lamsfoundation.lams.rest.RestTags; import org.lamsfoundation.lams.rest.ToolRestManager; import org.lamsfoundation.lams.tool.Tool; @@ -69,7 +73,15 @@ /** * Base class for actions processing Learning Design templates. * - * TODO stop hardcoding the icons! + * A little history: This code was written when we were still using Struts but we were phasing it out. So it was written with the + * minimal of Struts code, which was then swapped to being the minimal Spring MVC. Now we are using Spring MVC it would + * be nice to convert to using more Spring MVC features rather than adding parts piecemeal as the template pages + * become more complicated. Rather than relying on the web page to keep all the data and doing ajax updates, it would be + * nice to have a session form or the like backing the page. It would make it easier to have the templates load an existing + * design from the database for modification and make the processing when the template is saved so it doesn't do one huge + * parse in one go. + * + * Would also be nice to stop hardcoding the icons! * * @author Marcin Cieslak, Fiona Malikoff */ @@ -184,6 +196,12 @@ return "authoring/template/tbl/tbl"; } + public String init(HttpServletRequest request, String forward ) throws Exception { + String contentFolderID = FileUtil.generateUniqueContentFolderID(); + request.setAttribute(RestTags.CONTENT_FOLDER_ID, contentFolderID); + return forward; + } + protected abstract ObjectNode createLearningDesign(HttpServletRequest request, HttpSession httpSession) throws Exception; @@ -360,7 +378,7 @@ } /* ************************************** Non-Tool Activity methods ******************************************** */ - protected ObjectNode createGateActivity(AtomicInteger uiid, int order, Integer[] layoutCoords) { + protected ObjectNode createGateActivity(AtomicInteger uiid, int order, Integer[] layoutCoords, String activityTitle) { ObjectNode activityJSON = JsonNodeFactory.instance.objectNode(); Integer[] pos = layoutCoords; @@ -376,14 +394,24 @@ activityJSON.put(AuthoringJsonTags.APPLY_GROUPING, false); activityJSON.put(AuthoringJsonTags.XCOORD, pos[0]); activityJSON.put(AuthoringJsonTags.YCOORD, pos[1]); - activityJSON.put(AuthoringJsonTags.ACTIVITY_TITLE, "Gate"); // I18N + activityJSON.put(AuthoringJsonTags.ACTIVITY_TITLE, activityTitle != null ? activityTitle : "Gate"); activityJSON.put(AuthoringJsonTags.ACTIVITY_CATEGORY_ID, Activity.CATEGORY_SYSTEM); activityJSON.put(AuthoringJsonTags.ACTIVITY_TYPE_ID, Activity.PERMISSION_GATE_ACTIVITY_TYPE); activityJSON.put(AuthoringJsonTags.GATE_ACTIVITY_LEVEL_ID, GateActivity.LEARNER_GATE_LEVEL); return activityJSON; } + protected ObjectNode createScheduledGateActivity(AtomicInteger uiid, int order, Integer[] layoutCoords, + String activityTitle, Long startOffset) { + + ObjectNode activityJSON = createGateActivity(uiid, order, layoutCoords, activityTitle); + activityJSON.put(AuthoringJsonTags.ACTIVITY_TYPE_ID, Activity.SCHEDULE_GATE_ACTIVITY_TYPE); + activityJSON.put(AuthoringJsonTags.GATE_START_OFFSET, startOffset); + + return activityJSON; + } + /** Create a group activity's JSON objects */ protected ObjectNode[] createGroupingActivity(AtomicInteger uiid, int order, Integer[] layoutCoords, Integer groupingTypeID, Integer numLearners, Integer numGroups, String title, String[] groupNames, @@ -1306,6 +1334,60 @@ } + @RequestMapping("/importQTI") + public String importAssessmentQTI(HttpServletRequest request) throws UnsupportedEncodingException { + String contentFolderID = WebUtil.readStrParam(request, "contentFolderID"); + String templatePage = WebUtil.readStrParam(request, "templatePage"); + Question[] updatedQuestions = preprocessQuestions(QuestionParser.parseQuestionChoiceForm(request), contentFolderID); + request.setAttribute("questions", updatedQuestions); + request.setAttribute("questionNumber", WebUtil.readIntParam(request, "questionNumber")); + return "/authoring/template/tool/" + templatePage; + } + + private Question[] preprocessQuestions(Question[] questions, String contentFolderID) { + + // Processing based on QTIUtil from the Assessment tool + for ( Question question : questions ) { + + String correctAnswer = null; + boolean isMultipleChoice = Question.QUESTION_TYPE_MULTIPLE_CHOICE.equals(question.getType()); + boolean isMarkHedgingType = Question.QUESTION_TYPE_MARK_HEDGING.equals(question.getType()); + // int questionGrade = 1; Currently not supported by the templates. + + if (question.getAnswers() != null) { + for (Answer answer : question.getAnswers()) { + + String answerText = QuestionParser.processHTMLField(answer.getText(), false, contentFolderID, + question.getResourcesFolderPath()); + answer.setText(answerText); + + if ((correctAnswer != null) && correctAnswer.equals(answerText)) { + log.warn("Skipping an answer with same text as the correct answer: " + answerText); + continue; + } + if ((answer.getScore() != null) && (answer.getScore() > 0)) { + // for fill in blanks question all answers are correct and get full grade + if (!isMultipleChoice && !isMarkHedgingType || correctAnswer == null) { + // whatever the correct answer holds, it becomes the question score + // questionGrade = Double.valueOf(Math.ceil(answer.getScore())).intValue(); + // 100% goes to the correct answer + answer.setScore(1F); + correctAnswer = answerText; + } else { + log.warn("Choosing only first correct answer, despite another one was found: " + + answerText); + answer.setScore(0F); + } + } else { + answer.setScore(0F); + } + + } + } + } + return questions; + } + /** * Specialised call to create a new question & options for the surveys tab. Returns a fragment of HTML * which sets up a new CKEditor. Works with both mcquestion.jsp & surveyquestion.jsp. The template's Index: lams_central/src/java/org/lamsfoundation/lams/authoring/template/web/TBLTemplateController.java =================================================================== diff -u -r54007f98ca71e0073f19c5db78536437123287c6 -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/src/java/org/lamsfoundation/lams/authoring/template/web/TBLTemplateController.java (.../TBLTemplateController.java) (revision 54007f98ca71e0073f19c5db78536437123287c6) +++ lams_central/src/java/org/lamsfoundation/lams/authoring/template/web/TBLTemplateController.java (.../TBLTemplateController.java) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -22,6 +22,10 @@ */ package org.lamsfoundation.lams.authoring.template.web; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.Enumeration; import java.util.List; import java.util.Map; @@ -53,14 +57,18 @@ import com.fasterxml.jackson.databind.node.ObjectNode; /** - * A Team Based Learning template. + * A Team Based Learning template. There are two versions - the full blown one that is accessed from authoring + * and one that allows just sections of a TBL sequence to be created for the LAMS TBL system. Which bits are created + * are controlled by a series of checkboxes (set to true and hidden in the full authoring version). The LAMS TBL + * version also has the option for timed gates, but the standard version uses permission gates. */ @Controller @RequestMapping("authoring/template/tbl") public class TBLTemplateController extends LdTemplateController { private static Logger log = Logger.getLogger(TBLTemplateController.class); private static String templateCode = "TBL"; + private static final DateFormat LESSON_SCHEDULING_DATETIME_FORMAT = new SimpleDateFormat("MM/dd/yy HH:mm"); /** * Sets up the CKEditor stuff @@ -69,7 +77,15 @@ @RequestMapping("/init") public String init(HttpServletRequest request) throws Exception { request.setAttribute("questionNumber", "1"); - return super.init(request); + boolean isConfigurableVersion = WebUtil.readBooleanParam(request, "configure", false); + String sessionMapID = WebUtil.readStrParam(request, "sessionMapID", true); + if (sessionMapID != null) { + request.setAttribute("sessionMapID", sessionMapID); + } + if (isConfigurableVersion) + return super.init(request, "authoring/template/tbl/tbloptional"); + else + return super.init(request); } @Override @@ -93,116 +109,158 @@ ArrayNode groupings = JsonNodeFactory.instance.arrayNode(); Integer[] firstActivityInRowPosition = new Integer[] { 20, 125 }; // the very first activity, all other locations can be calculated from here if needed! + String activityTitle = null; + Integer[] currentActivityPosition = null; + Integer groupingUIID = null; // Welcome - String activityTitle = data.getText("boilerplate.introduction.title"); - Long welcomeToolContentId = createNoticeboardToolContent(userDTO, activityTitle, - data.getText("boilerplate.introduction.instructions"), null); - activities.add(createNoticeboardActivity(maxUIID, order++, firstActivityInRowPosition, welcomeToolContentId, - data.contentFolderID, null, null, null, activityTitle)); + if (data.useIntroduction) { + activityTitle = data.getText("boilerplate.introduction.title"); + Long welcomeToolContentId = createNoticeboardToolContent(userDTO, activityTitle, + data.getText("boilerplate.introduction.instructions"), null); + activities.add(createNoticeboardActivity(maxUIID, order++, firstActivityInRowPosition, welcomeToolContentId, + data.contentFolderID, null, null, null, activityTitle)); + currentActivityPosition = calcPositionNextRight(firstActivityInRowPosition); + } else { + currentActivityPosition = firstActivityInRowPosition; + } // Grouping - Integer[] currentActivityPosition = calcPositionNextRight(firstActivityInRowPosition); ObjectNode[] groupingJSONs = createGroupingActivity(maxUIID, order++, currentActivityPosition, data.groupingType, data.numLearners, data.numGroups, null, null, data.getUIBundle(), data.getFormatter()); activities.add(groupingJSONs[0]); groupings.add(groupingJSONs[1]); - Integer groupingUIID = groupingJSONs[1].get("groupingUIID").asInt(); + groupingUIID = groupingJSONs[1].get("groupingUIID").asInt(); - // Stop! - currentActivityPosition = calcPositionNextRight(currentActivityPosition); - activities.add(createGateActivity(maxUIID, order++, calcGateOffset(currentActivityPosition))); + if (data.useIRATRA) { + // Stop! + currentActivityPosition = calcPositionNextRight(currentActivityPosition); + activityTitle = data.getText("boilerplate.before.ira.gate"); + if (data.useScheduledGates && data.iraStartOffset != null) { + activities.add(createScheduledGateActivity(maxUIID, order++, calcGateOffset(currentActivityPosition), + activityTitle, data.iraStartOffset)); + } else { + activities.add( + createGateActivity(maxUIID, order++, calcGateOffset(currentActivityPosition), activityTitle)); + } - // iRA Test - MCQ - currentActivityPosition = calcPositionNextRight(currentActivityPosition); - activityTitle = data.getText("boilerplate.ira.title"); - Long iRAToolContentId = createMCQToolContent(userDTO, activityTitle, - data.getText("boilerplate.ira.instructions"), false, data.confidenceLevelEnable, - JsonUtil.readArray(data.testQuestions.values())); - ObjectNode iraActivityJSON = createMCQActivity(maxUIID, order++, currentActivityPosition, iRAToolContentId, - data.contentFolderID, groupingUIID, null, null, activityTitle); - activities.add(iraActivityJSON); + // iRA Test - MCQ + currentActivityPosition = calcPositionNextRight(currentActivityPosition); + activityTitle = data.getText("boilerplate.ira.title"); + Long iRAToolContentId = createMCQToolContent(userDTO, activityTitle, + data.getText("boilerplate.ira.instructions"), false, data.confidenceLevelEnable, + JsonUtil.readArray(data.testQuestions.values())); + ObjectNode iraActivityJSON = createMCQActivity(maxUIID, order++, currentActivityPosition, iRAToolContentId, + data.contentFolderID, groupingUIID, null, null, activityTitle); + activities.add(iraActivityJSON); - // Stop! - currentActivityPosition = calcPositionNextRight(currentActivityPosition); - activities.add(createGateActivity(maxUIID, order++, calcGateOffset(currentActivityPosition))); + // Stop! + firstActivityInRowPosition = calcPositionBelow(firstActivityInRowPosition); + currentActivityPosition = firstActivityInRowPosition; + activityTitle = data.getText("boilerplate.before.tra.gate"); + if (data.useScheduledGates && data.traStartOffset != null) { + activities.add(createScheduledGateActivity(maxUIID, order++, calcGateOffset(currentActivityPosition), + activityTitle, data.traStartOffset)); + } else { + activities.add(createGateActivity(maxUIID, order++, calcGateOffset(currentActivityPosition), + activityTitle)); + } - // Leader Selection - firstActivityInRowPosition = calcPositionBelow(firstActivityInRowPosition); - activityTitle = data.getText("boilerplate.leader.title"); - Long leaderSelectionToolContentId = createLeaderSelectionToolContent(userDTO, activityTitle, - data.getText("boilerplate.leader.instructions")); - activities.add(createLeaderSelectionActivity(maxUIID, order++, firstActivityInRowPosition, - leaderSelectionToolContentId, data.contentFolderID, groupingUIID, null, null, activityTitle)); + // Leader Selection + currentActivityPosition = calcPositionNextRight(currentActivityPosition); + activityTitle = data.getText("boilerplate.leader.title"); + Long leaderSelectionToolContentId = createLeaderSelectionToolContent(userDTO, activityTitle, + data.getText("boilerplate.leader.instructions")); + activities.add(createLeaderSelectionActivity(maxUIID, order++, currentActivityPosition, + leaderSelectionToolContentId, data.contentFolderID, groupingUIID, null, null, activityTitle)); - // tRA Test - currentActivityPosition = calcPositionNextRight(firstActivityInRowPosition); - activityTitle = data.getText("boilerplate.tra.title"); - Integer confidenceLevelsActivityUIID = data.confidenceLevelEnable - ? JsonUtil.optInt(iraActivityJSON, AuthoringJsonTags.ACTIVITY_UIID) - : null; - Long tRAToolContentId = createScratchieToolContent(userDTO, activityTitle, - data.getText("boilerplate.tra.instructions"), false, confidenceLevelsActivityUIID, - JsonUtil.readArray(data.testQuestions.values())); - activities.add(createScratchieActivity(maxUIID, order++, currentActivityPosition, tRAToolContentId, - data.contentFolderID, groupingUIID, null, null, activityTitle)); + // tRA Test + currentActivityPosition = calcPositionNextRight(currentActivityPosition); + activityTitle = data.getText("boilerplate.tra.title"); + Integer confidenceLevelsActivityUIID = data.confidenceLevelEnable + ? JsonUtil.optInt(iraActivityJSON, AuthoringJsonTags.ACTIVITY_UIID) + : null; + Long tRAToolContentId = createScratchieToolContent(userDTO, activityTitle, + data.getText("boilerplate.tra.instructions"), false, confidenceLevelsActivityUIID, + JsonUtil.readArray(data.testQuestions.values())); + activities.add(createScratchieActivity(maxUIID, order++, currentActivityPosition, tRAToolContentId, + data.contentFolderID, groupingUIID, null, null, activityTitle)); - // Stop! - currentActivityPosition = calcPositionNextRight(currentActivityPosition); - activities.add(createGateActivity(maxUIID, order++, calcGateOffset(currentActivityPosition))); + } - // Application Exercise - all the questions go into one exercise - firstActivityInRowPosition = calcPositionBelow(firstActivityInRowPosition); - currentActivityPosition = firstActivityInRowPosition; - ArrayNode questionsJSONArray = JsonNodeFactory.instance.arrayNode(); int displayOrder = 1; - for (Assessment exerciseQuestion : data.applicationExercises.values()) { - String applicationExerciseTitle = data.getText("boilerplate.ae.application.exercise.num", - new String[] { Integer.toString(displayOrder) }); - exerciseQuestion.setTitle(applicationExerciseTitle); - questionsJSONArray.add(exerciseQuestion.getAsObjectNode(displayOrder)); - displayOrder++; - } - String overallAssessmentTitle = data.getText("boilerplate.ae.application.exercise.num", new String[] { "" }); - Long aetoolContentId = createAssessmentToolContent(userDTO, overallAssessmentTitle, - data.getText("boilerplate.ae.instructions"), null, true, questionsJSONArray); - activities.add(createAssessmentActivity(maxUIID, order++, currentActivityPosition, aetoolContentId, - data.contentFolderID, groupingUIID, null, null, overallAssessmentTitle)); + if (data.useApplicationExercises) { + // Stop! + firstActivityInRowPosition = calcPositionBelow(firstActivityInRowPosition); + currentActivityPosition = firstActivityInRowPosition; + activityTitle = data.getText("boilerplate.before.app.ex"); + if (data.useScheduledGates && data.aeStartOffset != null) { + activities.add(createScheduledGateActivity(maxUIID, order++, calcGateOffset(currentActivityPosition), + activityTitle, data.aeStartOffset)); + } else { + activities.add( + createGateActivity(maxUIID, order++, calcGateOffset(currentActivityPosition), activityTitle)); + } - // Peer Review - optional. Start by eliminating all criterias with no title. Then if any are left - // we create the tool data and activity - currentActivityPosition = calcPositionNextRight(currentActivityPosition); - ArrayNode criterias = JsonNodeFactory.instance.arrayNode(); - for (PeerReviewCriteria criteria : data.peerReviewCriteria.values()) { - if (criteria.getTitle() != null && criteria.getTitle().length() > 0) { - criterias.add(criteria.getAsObjectNode()); + // Application Exercise - all the questions go into one exercise + currentActivityPosition = calcPositionNextRight(currentActivityPosition); + ArrayNode questionsJSONArray = JsonNodeFactory.instance.arrayNode(); + for (Assessment exerciseQuestion : data.applicationExercises.values()) { + if ( exerciseQuestion.getTitle() == null ) { + // put in a dummy title if one is not supplied by the user + String applicationExerciseTitle = data.getText("boilerplate.ae.application.exercise.num", + new String[] { Integer.toString(displayOrder) }); + exerciseQuestion.setTitle(applicationExerciseTitle); + } + questionsJSONArray.add(exerciseQuestion.getAsObjectNode(displayOrder)); + displayOrder++; } + String overallAssessmentTitle = data.getText("boilerplate.ae.application.exercise.num", + new String[] { "" }); + Long aetoolContentId = createAssessmentToolContent(userDTO, overallAssessmentTitle, + data.getText("boilerplate.ae.instructions"), null, true, questionsJSONArray); + activities.add(createAssessmentActivity(maxUIID, order++, currentActivityPosition, aetoolContentId, + data.contentFolderID, groupingUIID, null, null, overallAssessmentTitle)); } - if (criterias.size() > 0) { - String peerReviewTitle = data.getText("boilerplate.peerreview"); - Long prtoolContentId = createPeerReviewToolContent(userDTO, peerReviewTitle, - data.getText("boilerplate.peerreview.instructions"), null, criterias); - activities.add(createPeerReviewActivity(maxUIID, order++, currentActivityPosition, prtoolContentId, - data.contentFolderID, groupingUIID, null, null, peerReviewTitle)); - displayOrder++; + + if (data.usePeerReview) { + // Peer Review - optional. Start by eliminating all criterias with no title. Then if any are left + // we create the tool data and activity currentActivityPosition = calcPositionNextRight(currentActivityPosition); + ArrayNode criterias = JsonNodeFactory.instance.arrayNode(); + for (PeerReviewCriteria criteria : data.peerReviewCriteria.values()) { + if (criteria.getTitle() != null && criteria.getTitle().length() > 0) + criterias.add(criteria.getAsObjectNode()); + } + if (criterias.size() > 0) { + String peerReviewTitle = data.getText("boilerplate.peerreview"); + Long prtoolContentId = createPeerReviewToolContent(userDTO, peerReviewTitle, + data.getText("boilerplate.peerreview.instructions"), null, criterias); + activities.add(createPeerReviewActivity(maxUIID, order++, currentActivityPosition, prtoolContentId, + data.contentFolderID, groupingUIID, null, null, peerReviewTitle)); + displayOrder++; + currentActivityPosition = calcPositionNextRight(currentActivityPosition); + } } - // Stop! - activities.add(createGateActivity(maxUIID, order++, calcGateOffset(currentActivityPosition))); + if (data.useReflection) { + // Stop! + activities.add(createGateActivity(maxUIID, order++, calcGateOffset(currentActivityPosition), null)); - // Individual Reflection - currentActivityPosition = calcPositionNextRight(currentActivityPosition); - activityTitle = data.getText("boilerplate.individual.reflection.title"); - Long reflectionToolContentId = createNotebookToolContent(userDTO, activityTitle, - data.getText("boilerplate.individual.reflection.instructions"), false, false); - activities.add(createNotebookActivity(maxUIID, order++, currentActivityPosition, reflectionToolContentId, - data.contentFolderID, null, null, null, activityTitle)); + // Individual Reflection + currentActivityPosition = calcPositionNextRight(currentActivityPosition); + activityTitle = data.getText("boilerplate.individual.reflection.title"); + Long reflectionToolContentId = createNotebookToolContent(userDTO, activityTitle, + data.getText("boilerplate.individual.reflection.instructions"), false, false); + activities.add(createNotebookActivity(maxUIID, order++, currentActivityPosition, reflectionToolContentId, + data.contentFolderID, null, null, null, activityTitle)); - ArrayNode transitions = createTransitions(maxUIID, activities); + } // fill in required LD data and send it to the LearningDesignRestServlet + ArrayNode transitions = createTransitions(maxUIID, activities); + return saveLearningDesign(templateCode, data.sequenceTitle, "", workspaceFolderID, data.contentFolderID, maxUIID.get(), activities, transitions, groupings, null); @@ -227,6 +285,25 @@ Integer groupingType; Integer numLearners; Integer numGroups; + + Long iraGateStartOffset = null; + Long traGateStartOffset = null; + Long aeGateStartOffset = null; + + // used to configure which parts to use for the TBL system + boolean useFirstGateBeforeGrouping = false; + boolean useIntroduction = false; + boolean useIRATRA = false; + boolean useApplicationExercises = false; + boolean usePeerReview = false; + boolean useReflection = false; + + boolean useScheduledGates = false; + Long startTime = null; + Long iraStartOffset = null; + Long traStartOffset = null; + Long aeStartOffset = null; + SortedMap testQuestions; boolean confidenceLevelEnable; SortedMap applicationExercises; @@ -250,11 +327,39 @@ applicationExercises = new TreeMap(); peerReviewCriteria = new TreeMap(); + useIntroduction = WebUtil.readBooleanParam(request, "introduction", false); + useIRATRA = WebUtil.readBooleanParam(request, "iratra", false); + useApplicationExercises = WebUtil.readBooleanParam(request, "appex", false); + usePeerReview = WebUtil.readBooleanParam(request, "preview", false); + useReflection = WebUtil.readBooleanParam(request, "reflect", false); + + String start = WebUtil.readStrParam(request, "startLogistic", true); + if ( start != null ) { + // we are using the TBL version of LAMS to start the lessons, so we need to use schedule gates + // and set the timings of the gates. Need to take into account if the lesson is started now or in the future. + useScheduledGates = true; + String dateTimeString = WebUtil.readStrParam(request, "lessonStartDatetime", true); + if (start.equals("startNow") || dateTimeString == null || dateTimeString.length() == 0) { + startTime = System.currentTimeMillis(); + } else { + try { + Date startDate = TBLTemplateController.LESSON_SCHEDULING_DATETIME_FORMAT.parse(dateTimeString); + startTime = startDate.getTime(); + } catch (ParseException e) { + log.error("Unable to parse date from " + dateTimeString + ". Leaving start date as now."); + startTime = System.currentTimeMillis(); + } + } + iraStartOffset = getOffsetFromRequest(request, "iraLogistic", "iraScheduled", "iraStartDatetime"); + traStartOffset = getOffsetFromRequest(request, "traLogistic", "traScheduled", "traStartDatetime"); + aeStartOffset = getOffsetFromRequest(request, "aeLogistic", "aeScheduled", "aeStartDatetime"); + } + TreeMap correctAnswers = new TreeMap(); Enumeration parameterNames = request.getParameterNames(); while (parameterNames.hasMoreElements()) { String name = (String) parameterNames.nextElement(); - if (name.startsWith("question")) { + if (useIRATRA && name.startsWith("question")) { int correctIndex = name.indexOf("correct"); if (correctIndex > 0) { // question1correct Integer questionDisplayOrder = Integer.valueOf(name.substring(questionOffset, correctIndex)); @@ -273,27 +378,53 @@ null); } } - } else if (name.startsWith("peerreview")) { + } else if (usePeerReview && name.startsWith("peerreview")) { processInputPeerReviewRequestField(name, request); } } - confidenceLevelEnable = WebUtil.readBooleanParam(request, "confidenceLevelEnable", false); - updateCorrectAnswers(correctAnswers); - processAssessments(request); + confidenceLevelEnable = WebUtil.readBooleanParam(request, "confidenceLevelEnable", false); + if (useIRATRA) { + updateCorrectAnswers(correctAnswers); + } + if (useApplicationExercises) { + processAssessments(request); + } + validate(); } + private Long getOffsetFromRequest(HttpServletRequest request, String radioButtonField, + String radioButtonScheduledString, String dateField) { + String radioValue = WebUtil.readStrParam(request, radioButtonField, true); + if (radioButtonScheduledString.equals(radioValue)) { + String dateTimeString = WebUtil.readStrParam(request, dateField); + if (dateTimeString != null) { + try { + Long offset = TBLTemplateController.LESSON_SCHEDULING_DATETIME_FORMAT.parse(dateTimeString).getTime() + - this.startTime; + return offset > 0 ? offset / 60000 : 0; // from ms to minutes + } catch (ParseException e) { + log.error("Unable to parse date from " + dateField + + ". Leaving gate as a permission gate to be opened manually."); + } + } + } + return null; + } + // Assessment entries can't be deleted or rearranged so the count should always line up with numAssessments private void processAssessments(HttpServletRequest request) { int numAssessments = WebUtil.readIntParam(request, "numAssessments"); for (int i = 1; i <= numAssessments; i++) { String assessmentPrefix = "assessment" + i; String questionText = getTrimmedString(request, assessmentPrefix, true); + String questionTitle = getTrimmedString(request, assessmentPrefix+"title", true); Assessment assessment = new Assessment(); if (questionText != null) { + assessment.setTitle(questionTitle); assessment.setQuestionText(questionText); assessment.setType(WebUtil.readStrParam(request, assessmentPrefix + "type")); assessment.setRequired(true); @@ -420,15 +551,17 @@ } if (sequenceTitle == null || !ValidationUtil.isOrgNameValid(sequenceTitle)) { addValidationErrorMessage("authoring.fla.title.validation.error", null); - } - if (applicationExercises.size() == 0) { - addValidationErrorMessage("authoring.error.application.exercise.num", new Integer[] { 1 }); - } else { - for (Map.Entry assessmentEntry : applicationExercises.entrySet()) { - List errors = assessmentEntry.getValue().validate(appBundle, formatter, - assessmentEntry.getKey()); - if (errors != null) { - errorMessages.addAll(errors); + } + + if (useApplicationExercises) { + if (applicationExercises.size() == 0) { + addValidationErrorMessage("authoring.error.application.exercise.num", new Integer[] { 1 }); + } else { + for (Map.Entry assessmentEntry : applicationExercises.entrySet()) { + List errors = assessmentEntry.getValue().validate(appBundle, formatter, + assessmentEntry.getKey()); + if (errors != null) + errorMessages.addAll(errors); } } } Index: lams_central/src/java/org/lamsfoundation/lams/web/QuestionsController.java =================================================================== diff -u -r7c0aefd996982f4c4412973df9695d5c3fed8635 -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/src/java/org/lamsfoundation/lams/web/QuestionsController.java (.../QuestionsController.java) (revision 7c0aefd996982f4c4412973df9695d5c3fed8635) +++ lams_central/src/java/org/lamsfoundation/lams/web/QuestionsController.java (.../QuestionsController.java) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -35,7 +35,7 @@ @RequestMapping("/questions") public String execute(@RequestParam(name = "file", required = false) MultipartFile file, - @RequestParam String returnURL, @RequestParam("limitType") String limitTypeParam, + @RequestParam String returnURL, @RequestParam("limitType") String limitTypeParam, @RequestParam String callerID, HttpServletRequest request) throws Exception { String tempDirName = Configuration.get(ConfigurationKeys.LAMS_TEMP_DIR); @@ -54,6 +54,10 @@ // this parameter is not really used at the moment request.setAttribute("returnURL", returnURL); + // this parameter is used by the authoring templates. TBL uses QTI import for both the Questions and Assessments tab + request.setAttribute("callerID", callerID); + + // show only chosen types of questions request.setAttribute("limitType", limitTypeParam); Index: lams_central/web/authoring/template/comms.jsp =================================================================== diff -u -r62aaf160878735888d077bf28fac3c1989bb8fbd -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/web/authoring/template/comms.jsp (.../comms.jsp) (revision 62aaf160878735888d077bf28fac3c1989bb8fbd) +++ lams_central/web/authoring/template/comms.jsp (.../comms.jsp) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -2,17 +2,22 @@ var validator; // configure the wizard. Call after doing var validator = $("#templateForm").validate({ - function initializeWizard(validatorIn) { + // screens need to specify the tabNumber for the save button if they are hiding tabs + function initializeWizard(validatorIn, startValidationOnTab, tblVersion) { validator = validatorIn; + + if ( typeof startValidationOnTab === "undefined" ) + startValidationOnTab = 0; + $('#rootwizard').bootstrapWizard({ 'nextSelector': '.button-next', 'previousSelector': '.button-previous', 'onTabShow': function(tab, navigation, index) { - var total = navigation.find('li').length; + var total = navigation.find('li[display = block]').length; var current = index+1; var percent = (current/total) * 100; $('#rootwizard .progress-bar').css({width:percent+'%'}) - if ( current == total) { + if ( navigation.find('li:last').get(0) == tab.get(0) ) { $('.button-save').show(); $('.button-next').hide(); } else { @@ -22,7 +27,7 @@ }, 'tabClass': 'nav nav-pills', 'onNext': function(tab, navigation, index) { - if ( index > 1 ) { + if ( index >= (startValidationOnTab + 1) ) { var valid = $("#templateForm").valid(); if(!valid) { validator.focusInvalid(); @@ -378,3 +383,12 @@ } return true; } + + + function importQTI(callerID, limit){ + var url = 'questions/questionFile.jsp?callerID='+callerID; + if ( limit ) { + url = url + '&limitType='+limit; + } + window.open(url,'QuestionFile','width=500,height=240,scrollbars=yes'); + } Index: lams_central/web/authoring/template/tbl/tbl.jsp =================================================================== diff -u -r1e612192adc45476d007a3254ee9d1903158c064 -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/web/authoring/template/tbl/tbl.jsp (.../tbl.jsp) (revision 1e612192adc45476d007a3254ee9d1903158c064) +++ lams_central/web/authoring/template/tbl/tbl.jsp (.../tbl.jsp) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -11,6 +11,7 @@ + <%@ include file="../header.jsp" %> @@ -22,7 +23,8 @@ <%@ include file="../comms.jsp" %> var minimumWordsSpinnerArray = new Array(); // Peer Review Tab. - + var showTime = 100; // how long should it take a field to show/hide. + $(document).ready(function() { groupingChanged(); @@ -71,6 +73,49 @@ }); + + <%-- matching importQTI(limit) function is in comms.jsp --%> + function saveQTI(formHTML, formName, callerID) { + var form = $($.parseHTML(formHTML)); + + if ( callerID == 'assessment' ) { + var nextNum = +$('#numAssessments').val()+1; + var url=getSubmissionURL()+"/importQTI.do?contentFolderID=${contentFolderID}&templatePage=assessmentQTI&questionNumber="+nextNum; + $.ajaxSetup({ cache: true }); + $.ajax({ + type: "POST", + url: url, + data: form.serializeArray(), + success: function(response, status, xhr) { + if ( status == "error" ) { + console.log( xhr.status + " " + xhr.statusText ); + } else { + $('#divassessments').append(response); + $('#divassess'+nextNum)[0].scrollIntoView(); + } + } + }); + + } else { + var nextNum = +$('#numQuestions').val()+1; + var url=getSubmissionURL()+"/importQTI.do?contentFolderID=${contentFolderID}&templatePage=mcquestionQTI&questionNumber="+nextNum; + $.ajaxSetup({ cache: true }); + $.ajax({ + type: "POST", + url: url, + data: form.serializeArray(), + success: function(response, status, xhr) { + if ( status == "error" ) { + console.log( xhr.status + " " + xhr.statusText ); + } else { + $('#divquestions').append(response); + $('#divq'+nextNum)[0].scrollIntoView(); + } + } + }); + } + } + @@ -105,6 +150,16 @@
+
+ + + + + + +
@@ -120,21 +175,23 @@
+   + +
- +
-
- 1 - <%@ include file="../tool/mcquestion.jsp" %>
-
+ + + + + +
@@ -144,8 +201,12 @@
- - + + + + + +
Index: lams_central/web/authoring/template/tool/assessmcq.jsp =================================================================== diff -u -rf32bc3c30b8ea0463d84b4dbdc112f77a400297a -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/web/authoring/template/tool/assessmcq.jsp (.../assessmcq.jsp) (revision f32bc3c30b8ea0463d84b4dbdc112f77a400297a) +++ lams_central/web/authoring/template/tool/assessmcq.jsp (.../assessmcq.jsp) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -1,12 +1,14 @@ <%@ taglib uri="tags-lams" prefix="lams"%> <%@ taglib uri="tags-fmt" prefix="fmt"%> <%@ taglib uri="tags-core" prefix="c"%> +<%@ taglib uri="tags-function" prefix="fn" %> <%@ page import="org.lamsfoundation.lams.authoring.template.web.LdTemplateController"%> ${questionNumber eq 1 ? "class=\"input required\"" : "class=\"input\""} -<%-- Generic MCQ question for assessment. Expects an input of questionNumber, contentFolderID, and creates a text field field question${questionNumber} and three options --%> +<%-- Generic MCQ question for assessment. Expects an input of questionNumber, contentFolderID, and creates a text field field question${questionNumber} and three options. + Question, and hence question.title and question.text are optional and are only populated if QTI is used to start the questions. --%>
@@ -20,7 +22,8 @@
- + + @@ -37,10 +40,27 @@
+ + + + ${fn:length(question.answers)} + + ${numAnswers} + + ${answer.text} + ${loopStatus.count} + ${answer.score} +
+ <%@ include file="assessoption.jsp" %> +
+
+
+ + -
- 4 + +
1 <%@ include file="assessoption.jsp" %>
@@ -57,7 +77,10 @@ <%@ include file="assessoption.jsp" %>
- + + + +
Index: lams_central/web/authoring/template/tool/assessment.jsp =================================================================== diff -u -r7475d08afc280b5e2e5ddf04e8bf35e3166aaf80 -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/web/authoring/template/tool/assessment.jsp (.../assessment.jsp) (revision 7475d08afc280b5e2e5ddf04e8bf35e3166aaf80) +++ lams_central/web/authoring/template/tool/assessment.jsp (.../assessment.jsp) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -1,7 +1,8 @@ <%@ taglib uri="tags-lams" prefix="lams"%> <%@ taglib uri="tags-fmt" prefix="fmt"%> <%@ taglib uri="tags-core" prefix="c"%> -<%-- Generic assessment tool page. Expects an input of questionNumber & contentFolderID, and creates a field named assessment${questionNumber} suitable for a essay entry --%> +<%-- Generic assessment tool page. Expects an input of questionNumber & contentFolderID, and creates a field named assessment${questionNumber} suitable for a essay entry. + Question, and hence question.title and question.text are optional and are only populated if QTI is used to start the questions. --%>
@@ -15,7 +16,8 @@
- + +
\ No newline at end of file Index: lams_central/web/authoring/template/tool/assessmentQTI.jsp =================================================================== diff -u --- lams_central/web/authoring/template/tool/assessmentQTI.jsp (revision 0) +++ lams_central/web/authoring/template/tool/assessmentQTI.jsp (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -0,0 +1,29 @@ +<%@ taglib uri="tags-lams" prefix="lams"%> +<%@ taglib uri="tags-fmt" prefix="fmt"%> +<%@ taglib uri="tags-core" prefix="c"%> +<%-- Handles creation of multiple assessment questions based on QTI input. +Expects an input of questionNumber (which will be the question number of the first question) & contentFolderID, +which are passed on to the individual question jsps to generate the form fields. --%> +${questionNumber} + + +${currentNumber} +${question} + +
+ + + <%@ include file="../tool/assessmcq.jsp" %> + + + <%@ include file="../tool/assessment.jsp" %> + + +
+ +${currentNumber + 1} + +
+ Index: lams_central/web/authoring/template/tool/mcoption.jsp =================================================================== diff -u -r7475d08afc280b5e2e5ddf04e8bf35e3166aaf80 -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/web/authoring/template/tool/mcoption.jsp (.../mcoption.jsp) (revision 7475d08afc280b5e2e5ddf04e8bf35e3166aaf80) +++ lams_central/web/authoring/template/tool/mcoption.jsp (.../mcoption.jsp) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -15,14 +15,7 @@ - - - - - - - - + Index: lams_central/web/authoring/template/tool/mcquestion.jsp =================================================================== diff -u -rf32bc3c30b8ea0463d84b4dbdc112f77a400297a -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/web/authoring/template/tool/mcquestion.jsp (.../mcquestion.jsp) (revision f32bc3c30b8ea0463d84b4dbdc112f77a400297a) +++ lams_central/web/authoring/template/tool/mcquestion.jsp (.../mcquestion.jsp) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -1,9 +1,10 @@ <%@ taglib uri="tags-lams" prefix="lams"%> <%@ taglib uri="tags-fmt" prefix="fmt"%> <%@ taglib uri="tags-core" prefix="c"%> +<%@ taglib uri="tags-function" prefix="fn" %> <%@ page import="org.lamsfoundation.lams.authoring.template.web.LdTemplateController"%> -<%-- Generic Q&A question page. Expects an input of questionNumber, contentFolderID, and creates a text field field question${questionNumber} and three options --%> +<%-- Generic Q&A question page. Expects an input of questionNumber, contentFolderID, and creates a text field field question${questionNumber} and three options / as many as are need for a QTI import --%>
@@ -12,7 +13,7 @@
- + @@ -27,29 +28,48 @@
- +
- -
- - 4 - 1 - <%@ include file="mcoption.jsp" %> + + + ${fn:length(question.answers)} + + ${numAnswers} + + ${answer.text} + ${answer.score > 0} + ${loopStatus.count} +
+ <%@ include file="mcoption.jsp" %> +
+
+
+ + + +
+ + 4 + 1 + <%@ include file="mcoption.jsp" %> +
+
+ 2 + <%@ include file="mcoption.jsp" %> +
+
+ 3 + <%@ include file="mcoption.jsp" %> +
+
+ 4 + <%@ include file="mcoption.jsp" %> +
+
+ +
-
- 2 - <%@ include file="mcoption.jsp" %> -
-
- 3 - <%@ include file="mcoption.jsp" %> -
-
- 4 - <%@ include file="mcoption.jsp" %> -
-
- +
\ No newline at end of file Index: lams_central/web/authoring/template/tool/mcquestionQTI.jsp =================================================================== diff -u --- lams_central/web/authoring/template/tool/mcquestionQTI.jsp (revision 0) +++ lams_central/web/authoring/template/tool/mcquestionQTI.jsp (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -0,0 +1,22 @@ +<%@ taglib uri="tags-lams" prefix="lams"%> +<%@ taglib uri="tags-fmt" prefix="fmt"%> +<%@ taglib uri="tags-core" prefix="c"%> +<%-- Handles creation of multiple mcq questions based on QTI input. +Expects an input of questionNumber (which will be the question number of the first question) & contentFolderID, +which are passed on to the individual question jsps to generate the form fields. --%> +${questionNumber} + + +${currentNumber} +${question} + +
+ <%@ include file="../tool/mcquestion.jsp" %> +
+ +${currentNumber + 1} + +
+ Index: lams_central/web/authoring/template/tool/mcredooption.jsp =================================================================== diff -u -r7475d08afc280b5e2e5ddf04e8bf35e3166aaf80 -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/web/authoring/template/tool/mcredooption.jsp (.../mcredooption.jsp) (revision 7475d08afc280b5e2e5ddf04e8bf35e3166aaf80) +++ lams_central/web/authoring/template/tool/mcredooption.jsp (.../mcredooption.jsp) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -8,6 +8,7 @@
${option.displayOrder} ${option.text} + ${option.correct} <%@ include file="mcoption.jsp" %>
\ No newline at end of file Index: lams_central/web/questions/questionChoice.jsp =================================================================== diff -u -r7475d08afc280b5e2e5ddf04e8bf35e3166aaf80 -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/web/questions/questionChoice.jsp (.../questionChoice.jsp) (revision 7475d08afc280b5e2e5ddf04e8bf35e3166aaf80) +++ lams_central/web/questions/questionChoice.jsp (.../questionChoice.jsp) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -48,6 +48,7 @@ window.resizeTo(1152, 648); var returnURL = '${returnURL}'; + var callerID = '${callerID}'; function submitForm() { var anyQuestionsSelected = false; @@ -62,7 +63,7 @@ var form = $("#questionForm"); if (returnURL == '') { form.css('visibility', 'hidden'); - window.opener.saveQTI(form[0].outerHTML, 'questionForm'); + window.opener.saveQTI(form[0].outerHTML, 'questionForm', callerID); window.close(); } else { // this code is not really used at the moment, but it's available Index: lams_central/web/questions/questionFile.jsp =================================================================== diff -u -rae9912edeb82523d71d1e18df67ec5ee7e6301a8 -r3f18890e24de43c31ac23d49e2c1a4906e479863 --- lams_central/web/questions/questionFile.jsp (.../questionFile.jsp) (revision ae9912edeb82523d71d1e18df67ec5ee7e6301a8) +++ lams_central/web/questions/questionFile.jsp (.../questionFile.jsp) (revision 3f18890e24de43c31ac23d49e2c1a4906e479863) @@ -62,6 +62,7 @@
+