/// <summary> /// Requests the RloHandler to do whatever is required to exit from the current activity. /// This request may only be issued when the session is in Execute view and is not active -- it is /// either Completed or Abandoned. /// </summary> /// <param name="context">The context within which the command is processed</param> /// <remarks> /// This method should only be called for the <c>SessionView.Execute</c> view. However, /// no checks are done internally to verify this - if this is called with other views, /// unexpected results will occur. /// </remarks> public override void ProcessSessionEnd(RloDataModelContext context) { LearningDataModel learningDataModel = context.LearningDataModel; // Set ExitMode to suspend so that when a student exits the activity it is left in a suspended state. // This way if the activity is reactivated, the student's previous answers are intact. learningDataModel.NavigationRequest.ExitMode = ExitMode.Suspended; // If the page has never been visited, "visit" it. if (!GetPageHasBeenVisited(learningDataModel)) { AssessmentItemManager.DataModelContext = context; // Get the input stream containing the primary file from the resource associated with the // current activity in the session. using (Stream inputStream = context.GetInputStream()) { // find all the assessment items (<IMG> tags that contain the text "mslamrk" as part of the src attribute.) using (HtmlTextReader reader = new HtmlTextReader(inputStream)) { int srcIndex; while (reader.Read()) { if (IsAITag(reader, out srcIndex)) { try { AssessmentItem ai = AssessmentItem.Parse(reader.GetAttributeValue(srcIndex)); AssessmentItemRenderer renderer = AssessmentItemManager.GetRenderer(ai); renderer.TryAddToDataModel(); } catch (FormatException) { // skip this one. } } } } } SetPageHasBeenVisited(learningDataModel); } // If the page has never been autograded, call ProcessSessionEnd on the form data processors if (!GetPageHasBeenAutograded(learningDataModel)) { AssessmentItemManager.ProcessFormContext = new RloProcessFormDataContext(SessionView.Execute, learningDataModel); float? totalPoints = null; foreach (Interaction interaction in learningDataModel.Interactions) { FormDataProcessor processor = m_assessmentItemMgr.GetFormDataProcessor(interaction); // must check that processor is non null, since GetFormDataProcessor() can return null. // If it is null, any item score associated with this interaction is not totalled into // EvaluationPoints. if (processor != null) { processor.ProcessSessionEnd(context); if (interaction.Evaluation.Points.HasValue) { if (totalPoints.HasValue) { totalPoints += interaction.Evaluation.Points; } else { totalPoints = interaction.Evaluation.Points; } } } } learningDataModel.EvaluationPoints = totalPoints; SetPageHasBeenAutograded(learningDataModel); } }
/// <summary> /// Returns "true" if the reader's node is named the supplied name, case-insensitive. /// </summary> private static bool IsNamed(HtmlTextReader reader, string name) { return 0 == String.Compare(reader.Name, name, StringComparison.OrdinalIgnoreCase); }
/// <summary> /// Returns true if the reader is positioned on a node that has a bgColor="#ff99cb" attribute. /// </summary> private static bool IsBgColorFF99CB(HtmlTextReader reader) { if (reader.HasAttributes) { for (int i = 0; i < reader.AttributeCount; i++) { if (String.Compare("bgColor", reader.GetAttributeName(i), StringComparison.OrdinalIgnoreCase) == 0 && String.Compare("#ff99cb", reader.GetAttributeValue(i).ToString(), StringComparison.OrdinalIgnoreCase) == 0) { return true; } } } return false; }
/// <summary> /// Output the current node, or the changes required to the current node by the rendering code, to the writer. /// <img> nodes representing assessment items are handled elsewhere. /// </summary> private void HandleNode(HtmlTextReader reader, StreamWriter writer) { AIResources.Culture = LocalizationManager.GetCurrentCulture(); RloRenderContext context = AssessmentItemManager.RenderContext; if (reader.NodeType == HtmlNodeType.Element) { if (IsNamed(reader, "body")) { const string ecs = " ECS_ViewType=\"{0}\" leftmargin=0 topmargin=0 rightmargin=0 bottommargin=0 "; const string form = "<form NAME=\"{0}\" {1}METHOD=\"post\" ENCTYPE=\"multipart/form-data\" style=\"height:100%; width:100%; border:none; margin:0\">"; const string hidDetach = "<INPUT Name=\"hidDetach\" TYPE=\"HIDDEN\" value=\"0\">"; StringWriter bodyWriter = new StringWriter(CultureInfo.InvariantCulture); reader.CopyNode(bodyWriter); string body = bodyWriter.ToString(); bodyWriter.Close(); switch (context.View) { case SessionView.Execute: // update the <body> to include ECS_ViewType attribute and margin attributes body = body.Insert(5, String.Format(CultureInfo.InvariantCulture, ecs, "2")); break; case SessionView.RandomAccess: // update the <body> to include ECS_ViewType attribute and margin attributes body = body.Insert(5, String.Format(CultureInfo.InvariantCulture, ecs, "6")); break; case SessionView.Review: // update the <body> to include ECS_ViewType attribute and margin attributes body = body.Insert(5, String.Format(CultureInfo.InvariantCulture, ecs, "4")); break; } writer.Write(body); writer.Write(String.Format(CultureInfo.InvariantCulture, form, "frmPage", " ")); writer.Write(hidDetach); WriteFormHiddenControls(writer, context.FormHiddenControls); return; } // if ShowReviewerInformation is false, check all table elements for a bgColor="#ff99cb" // and remove them from display. else if (!context.ShowReviewerInformation && IsNamed(reader, "table") && IsBgColorFF99CB(reader)) { reader.Skip(); return; } } else if (reader.NodeType == HtmlNodeType.EndElement) { // insert a script section before the </head> if (IsNamed(reader, "head")) { switch (context.View) { case SessionView.Execute: writer.Write(AIResources.HeadExecuteViewScript); break; case SessionView.RandomAccess: // Security issue, FormElementId must be javascript safe. writer.Write(m_headGradingViewScript.Replace("<%=FormId%>", "frmPage")); break; case SessionView.Review: writer.Write(AIResources.ReviewViewScript); break; } reader.CopyNode(writer); return; } else if (IsNamed(reader, "body")) { // insert any RloRenderContext.ScriptToRender and the </form> before the </body> if (!String.IsNullOrEmpty(context.ScriptToRender)) { writer.WriteLine("<script>"); writer.WriteLine(context.ScriptToRender); writer.WriteLine("</script>"); } writer.Write("</form></body>"); return; } } reader.CopyNode(writer); }
/// <summary> /// Render the requested view into the output stream. /// </summary> /// <param name="context">The context within which to render the page.</param> /// <remarks> /// When this method returns the <paramref name="context"/> OutputStream will contain /// the rendered file. /// <p> /// The following methods and properties must be return valid values from /// the <paramref name="context"/>: /// <ul> /// <li>EmbeddedUiResourcePath, must be non-null</li> /// <li>FormElementId</li> /// <li>GetInputStream</li> /// <li>OutputStream</li> /// <li>View</li> /// </ul> /// </p> /// <p> /// Additionally, if the following properties are set, they will be used: /// <ul> /// <li>FormElementAction</li> /// <li>HiddenControls</li> /// <li>ScriptToRender</li> /// </ul> /// </p> /// All other properties on <paramref name="context"/> are ignored. /// </remarks> /// <exception cref="FileNotFoundException">The requested file attachment can't be found.</exception> public override void Render(RloRenderContext context) { AIResources.Culture = LocalizationManager.GetCurrentCulture(); // string is the key (which is AssessmentItem.Id_AssessmentItem.Type) // int is the ordinal (0 based) which is the number of times the key has been processed Dictionary<string, int> assessmentItems = new Dictionary<string, int>(); // The most common case is that the file is in the package Stream inputStream = null; AssessmentItemManager.DataModelContext = context; LearningDataModel learningDataModel = context.LearningDataModel; try { int srcIndex; // represents the index of the "src" attribute on an <img> node. // If this is the first time the page is being rendered, parse the page and determine // the interactions on the page. if (context.View == SessionView.Execute) { if (!GetPageHasBeenVisited(learningDataModel)) { using (inputStream = context.GetInputStream()) { // If the file being requested is the default file for the current activity, if (context.IsResourceEntryPoint) { // find all the assessment items (<IMG> tags that contain the text "mslamrk" as part of the src attribute.) using (HtmlTextReader reader = new HtmlTextReader(inputStream)) { while (reader.Read()) { if (IsAITag(reader, out srcIndex)) { try { AssessmentItem ai = AssessmentItem.Parse(reader.GetAttributeValue(srcIndex)); AssessmentItemRenderer renderer = AssessmentItemManager.GetRenderer(ai); renderer.TryAddToDataModel(); } catch (FormatException) { // skip this one. This is mirrored below in the 2nd pass. } } } } } } SetPageHasBeenVisited(learningDataModel); } } // must get the input stream again since it may not be possible to seek back to the beginning using (inputStream = context.GetInputStream()) { if (context.Response != null) { // Clear the output response context.Response.Clear(); } // If the file being requested is the default file for the current activity, if (context.IsResourceEntryPoint) { if (context.View == SessionView.Execute) { // Set ExitMode to suspend so that when a student exits the activity it is left in a suspended state. // This way if the activity is reactivated, the student's previous answers are intact. learningDataModel.NavigationRequest.ExitMode = ExitMode.Suspended; } DetachableStream detachable = new DetachableStream(context.OutputStream); // Parse through the input stream again, this time rendering into the output as we go. using (StreamWriter writer = new StreamWriter(detachable)) { using (HtmlTextReader reader = new HtmlTextReader(inputStream)) { while (reader.Read()) { if (IsAITag(reader, out srcIndex)) { try { AssessmentItem ai = AssessmentItem.Parse(reader.GetAttributeValue(srcIndex)); AssessmentItemRenderer renderer = AssessmentItemManager.GetRenderer(ai); if (assessmentItems.ContainsKey(ai.RenderKey)) { assessmentItems[ai.RenderKey] += 1; } else { assessmentItems.Add(ai.RenderKey, 0); } writer.Write(renderer.Render(assessmentItems[ai.RenderKey]).ToString()); } catch (FormatException) { // skip this one. This is mirrored above in the 1st pass. } } else { HandleNode(reader, writer); } } } // don't allow closing the StreamWriter to close the context.OutputStream. writer.Flush(); detachable.Detach(); } // set the response type context.SetOutputStreamExtension(Path.GetExtension(context.RelativePath)); } else { // for a non-entry-point file, copy the file directly to the output stream context.WriteFileToResponse(context.RelativePath); } } return; } catch (FileNotFoundException) { // This means the requested file is not in the package. That's not necessarily a problem, since it // may be a request for an attachment. } // We got here because the file is not in the package. In that case, render it if it is a file attachment int beginAttachmentInfo = context.RelativePath.IndexOf("/~RLO/", StringComparison.Ordinal); if (beginAttachmentInfo != -1) { // attachmentInfo should be of the form <interactionId>/<attachmentIndex>, so split it into the parts string attachmentInfo = context.RelativePath.Substring(beginAttachmentInfo + 6); RenderFileAttachment(context, attachmentInfo); } else { // This means the requested file is not in the package, nor is it a request for an attachment. throw new FileNotFoundException(AIResources.FileNotFound); } }
/// <summary> /// Returns true if the reader is positioned on an assessment item IMG tag and sets srcIndex to the index of the /// src attribute. /// </summary> private static bool IsAITag(HtmlTextReader reader, out int srcIndex) { if (reader.NodeType == HtmlNodeType.Element && String.Compare(reader.Name, "img", StringComparison.OrdinalIgnoreCase) == 0) { if (reader.HasAttributes) { for (int i = 0; i < reader.AttributeCount; i++) { if (String.Compare("src", reader.GetAttributeName(i), StringComparison.OrdinalIgnoreCase) == 0 && reader.GetAttributeValue(i).ToString().Contains("mslamrk")) { srcIndex = i; return true; } } } } srcIndex = -1; return false; }