Index: lams_bb_integration/RELEASE_NOTES.TXT =================================================================== diff -u -r46f6e2a2d8c2b6552718747c373df53efd0cfc42 -rb4a4a2a44c27e0307086d3405993f9febe39f13b --- lams_bb_integration/RELEASE_NOTES.TXT (.../RELEASE_NOTES.TXT) (revision 46f6e2a2d8c2b6552718747c373df53efd0cfc42) +++ lams_bb_integration/RELEASE_NOTES.TXT (.../RELEASE_NOTES.TXT) (revision b4a4a2a44c27e0307086d3405993f9febe39f13b) @@ -127,3 +127,4 @@ 1.2.19 Release Fixes ==================== * LDEV-3399: Course Copy feature. Remove auxiliary building block and introduce special button to update LAMS links with new lesson ids. +* LDEV-3897: Ability to fix LAMS lessons in imported courses Index: lams_bb_integration/WEB-INF/web.xml =================================================================== diff -u -r46f6e2a2d8c2b6552718747c373df53efd0cfc42 -rb4a4a2a44c27e0307086d3405993f9febe39f13b --- lams_bb_integration/WEB-INF/web.xml (.../web.xml) (revision 46f6e2a2d8c2b6552718747c373df53efd0cfc42) +++ lams_bb_integration/WEB-INF/web.xml (.../web.xml) (revision b4a4a2a44c27e0307086d3405993f9febe39f13b) @@ -24,8 +24,12 @@ CloneLessonsServlet org.lamsfoundation.ld.integration.blackboard.CloneLessonsServlet - + + ImportLessonsServlet + org.lamsfoundation.ld.integration.blackboard.ImportLessonsServlet + + GradebookServlet org.lamsfoundation.ld.integration.blackboard.GradebookServlet @@ -61,8 +65,12 @@ CloneLessonsServlet /CloneLessons - + + ImportLessonsServlet + /ImportLessons + + GroupDataServlet /GroupData Index: lams_bb_integration/src/org/lamsfoundation/ld/integration/blackboard/ImportLessonsServlet.java =================================================================== diff -u --- lams_bb_integration/src/org/lamsfoundation/ld/integration/blackboard/ImportLessonsServlet.java (revision 0) +++ lams_bb_integration/src/org/lamsfoundation/ld/integration/blackboard/ImportLessonsServlet.java (revision b4a4a2a44c27e0307086d3405993f9febe39f13b) @@ -0,0 +1,276 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + + +package org.lamsfoundation.ld.integration.blackboard; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.URLEncoder; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.log4j.Logger; +import org.lamsfoundation.ld.util.LamsServerException; + +import blackboard.base.BbList; +import blackboard.base.FormattedText; +import blackboard.base.InitializationException; +import blackboard.data.content.Content; +import blackboard.data.course.Course; +import blackboard.data.course.CourseMembership; +import blackboard.data.navigation.CourseToc; +import blackboard.data.user.User; +import blackboard.persist.Id; +import blackboard.persist.PkId; +import blackboard.persist.content.ContentDbLoader; +import blackboard.persist.content.ContentDbPersister; +import blackboard.persist.course.CourseDbLoader; +import blackboard.persist.course.CourseMembershipDbLoader; +import blackboard.persist.navigation.CourseTocDbLoader; +import blackboard.platform.BbServiceException; +import blackboard.platform.BbServiceManager; +import blackboard.platform.context.Context; +import blackboard.platform.context.ContextManager; +import blackboard.portal.data.ExtraInfo; +import blackboard.portal.data.PortalExtraInfo; +import blackboard.portal.servlet.PortalUtil; +import blackboard.util.StringUtil; + +/** + * Admin on BB side calls this servlet to import old lesson that were copied to the new course. + */ +public class ImportLessonsServlet extends HttpServlet { + + private static final long serialVersionUID = -3587062723412672184L; + private static Logger logger = Logger.getLogger(ImportLessonsServlet.class); + + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + + String courseIdParam = request.getParameter("courseId"); + if (StringUtil.isEmpty(courseIdParam)) { + throw new RuntimeException("Required parameters are missing. courseId: " + courseIdParam); + } + + String newLessonIds = ""; + try { + // get Blackboard context + ContextManager ctxMgr = (ContextManager) BbServiceManager.lookupService(ContextManager.class); + Context ctx = ctxMgr.setContext(request); + + CourseDbLoader courseLoader = CourseDbLoader.Default.getInstance(); + Course course = courseLoader.loadByCourseId(courseIdParam); + PkId courseId = (PkId) course.getId(); + String _course_id = "_" + courseId.getPk1() + "_" + courseId.getPk2(); + + // find the main teacher + CourseMembershipDbLoader courseMemLoader = CourseMembershipDbLoader.Default.getInstance(); + BbList monitorCourseMemberships = courseMemLoader.loadByCourseIdAndRole(courseId, + CourseMembership.Role.INSTRUCTOR, null, true); + if (monitorCourseMemberships.isEmpty()) { + BbList teachingAssistantCourseMemberships = courseMemLoader + .loadByCourseIdAndRole(courseId, CourseMembership.Role.TEACHING_ASSISTANT, null, true); + monitorCourseMemberships.addAll(teachingAssistantCourseMemberships); + if (monitorCourseMemberships.isEmpty()) { + BbList courseBuilderCourseMemberships = courseMemLoader + .loadByCourseIdAndRole(courseId, CourseMembership.Role.COURSE_BUILDER, null, true); + monitorCourseMemberships.addAll(courseBuilderCourseMemberships); + } + } + // validate teacher existence + if (monitorCourseMemberships.isEmpty()) { + throw new RuntimeException("There are no monitors in the course courseId=" + courseId); + } + User teacher = monitorCourseMemberships.get(0).getUser(); + + logger.debug("Starting importing course lessons (courseId=" + courseId + ")."); + + ContentDbLoader contentLoader = ContentDbLoader.Default.getInstance(); + CourseTocDbLoader cTocDbLoader = CourseTocDbLoader.Default.getInstance(); + ContentDbPersister persister =ContentDbPersister.Default.getInstance(); + + //find all lessons that should be updated + + // get a CourseTOC (Table of Contents) loader. We will need this to iterate through all of the "areas" + // within the course + BbList courseTocs = cTocDbLoader.loadByCourseId(courseId); + + // iterate through the course TOC items + for (CourseToc courseToc : courseTocs) { + + // determine if the TOC item is of type "CONTENT" rather than applicaton, or something else + if ((courseToc.getTargetType() == CourseToc.Target.CONTENT) + && (courseToc.getContentId() != Id.UNSET_ID)) { + // we have determined that the TOC item is content, next we need to load the content object and + // iterate through it + // load the content tree into an object "content" and iterate through it + BbList contents = contentLoader.loadListById(courseToc.getContentId()); + // iterate through the content items in this content object + for (Content content : contents) { + // only LAMS content + if ("resource/x-lams-lamscontent".equals(content.getContentHandler())) { + + PkId contentId = (PkId) content.getId(); + String _content_id = "_" + contentId.getPk1() + "_" + contentId.getPk2(); + + String url = content.getUrl(); + String urlLessonId = getParameterValue(url, "lsid"); + String urlCourseId = getParameterValue(url, "course_id"); + String urlContentId = getParameterValue(url, "content_id"); + String urlLdId = getParameterValue(url, "ldid"); + + //in case when both courseId and contentId don't coincide with the ones from URL - means lesson needs to be imported + if (!urlCourseId.equals(_course_id) && !urlContentId.equals(_content_id)) { + + final Long newLdId = LamsSecurityUtil.importLearningDesign(teacher, courseIdParam, urlLessonId, + urlLdId); + + logger.debug("Lesson (lessonId=" + urlLessonId + + ") was successfully imported to the one (learningDesignId=" + newLdId + ")."); + + // Start the Lesson in LAMS (via Webservices) and capture the lesson ID + String teacherUsername = teacher.getUserName(); + String title = content.getTitle(); + FormattedText descriptionFormatted = content.getBody(); + String description = URLEncoder.encode(descriptionFormatted.getText(), "UTF-8"); + final long newLessonId = LamsSecurityUtil.startLesson(ctx, teacherUsername, + courseIdParam, newLdId, title, description, false); + + // update lesson id + content.setLinkRef(Long.toString(newLessonId)); + + // update URL + url = replaceParameterValue(url, "lsid", Long.toString(newLessonId)); + url = replaceParameterValue(url, "course_id", _course_id); + url = replaceParameterValue(url, "content_id", _content_id); + content.setUrl(url); + + // persist updated content + persister.persist(content); + + // store internalContentId -> externalContentId. It's used for lineitem removal (delete.jsp) + PortalExtraInfo pei = PortalUtil.loadPortalExtraInfo(null, null, "LamsStorage"); + ExtraInfo ei = pei.getExtraInfo(); + ei.setValue(_content_id, Long.toString(newLessonId)); + PortalUtil.savePortalExtraInfo(pei); + + // Gradebook column will be copied automatically if appropriate option is selected on + // importing lesson page + + logger.debug("Lesson (lessonId=" + urlLessonId + + ") was successfully imported to the one (lessonId=" + newLessonId + ")."); + + newLessonIds += newLessonId + ", "; + } + } + + } + } + } + + } catch (LamsServerException e) { + //printing out error cause + response.setContentType("text/html"); + PrintWriter out = response.getWriter(); + out.write("Failed! " + e.getMessage()); + out.flush(); + out.close(); + return; + } catch (IllegalStateException e) { + throw new ServletException( + "LAMS Server timeout, did not get a response from the LAMS server. Please contact your systems administrator", + e); + } catch (InitializationException e) { + throw new ServletException(e); + } catch (BbServiceException e) { + throw new ServletException(e); + } catch (Exception e) { + throw new ServletException(e); + } + + //prepare string to write out + int newLessonsCounts = newLessonIds.length() - newLessonIds.replace(",", "").length(); + String resultStr = "Complete! " + newLessonsCounts + " lessons have been imported."; + //add all lessonIds (without the last comma) + if (newLessonsCounts > 0) { + resultStr += " Their updated lessonIds: " + newLessonIds.substring(0, newLessonIds.length()-2); + } + logger.debug(resultStr); + + response.setContentType("text/html"); + PrintWriter out = response.getWriter(); + out.write(resultStr); + out.flush(); + out.close(); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doGet(req, resp); + } + + /* + * Returns param value, and empty string in case of there is no such param available + * + * @param url + * @param paramName + * @return + */ + private static String getParameterValue(String url, String paramName) { + String paramValue = ""; + + int quotationMarkIndex = url.indexOf("?"); + String queryPart = quotationMarkIndex > -1 ? url.substring(quotationMarkIndex + 1) : url; + String[] paramEntries = queryPart.split("&"); + for (String paramEntry : paramEntries) { + String[] paramEntrySplitted = paramEntry.split("="); + if ((paramEntrySplitted.length > 1) && paramName.equalsIgnoreCase(paramEntrySplitted[0])) { + paramValue = paramEntrySplitted[1]; + break; + } + } + + return paramValue; + } + + private static String replaceParameterValue(String url, String paramName, String newParamValue) { + String oldParamValue = ""; + + int quotationMarkIndex = url.indexOf("?"); + String queryPart = quotationMarkIndex > -1 ? url.substring(quotationMarkIndex + 1) : url; + String[] paramEntries = queryPart.split("&"); + for (String paramEntry : paramEntries) { + String[] paramEntrySplitted = paramEntry.split("="); + if ((paramEntrySplitted.length > 1) && paramName.equalsIgnoreCase(paramEntrySplitted[0])) { + oldParamValue = paramEntrySplitted[1]; + + return url.replaceFirst(paramName + "=" + oldParamValue, paramName + "=" + newParamValue); + } + } + + return url; + } + +} \ No newline at end of file Index: lams_bb_integration/src/org/lamsfoundation/ld/integration/blackboard/LamsSecurityUtil.java =================================================================== diff -u -r46f6e2a2d8c2b6552718747c373df53efd0cfc42 -rb4a4a2a44c27e0307086d3405993f9febe39f13b --- lams_bb_integration/src/org/lamsfoundation/ld/integration/blackboard/LamsSecurityUtil.java (.../LamsSecurityUtil.java) (revision 46f6e2a2d8c2b6552718747c373df53efd0cfc42) +++ lams_bb_integration/src/org/lamsfoundation/ld/integration/blackboard/LamsSecurityUtil.java (.../LamsSecurityUtil.java) (revision b4a4a2a44c27e0307086d3405993f9febe39f13b) @@ -47,6 +47,7 @@ import org.apache.commons.io.IOUtils; import org.apache.log4j.Logger; import org.lamsfoundation.ld.integration.dto.LearnerProgressDTO; +import org.lamsfoundation.ld.util.LamsServerException; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.xml.sax.SAXException; @@ -76,6 +77,7 @@ private static Logger logger = Logger.getLogger(LamsSecurityUtil.class); private static final String DUMMY_COURSE = "Previews"; + private static final String EXPORT_FOLDER_LAMS_SERVER = "/tmp/lams/"; /** * Generates login requests to LAMS for author, monitor and learner, using the alternative URL. @@ -524,23 +526,14 @@ logger.info("LAMS START LESSON Req: " + serviceURL); - // InputStream is = url.openConnection().getInputStream(); + // parse xml response and get the lesson id InputStream is = LamsSecurityUtil.callLamsServerPost(serviceURL); - - // parse xml response DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document document = db.parse(is); - - // get the lesson id from the response - - /* - * The getTextContext is not a java 1.4 method, so Blackboard 7.1 comes up with errors using getNodeValue() - * instead - */ - // return Long.parseLong(document.getElementsByTagName("Lesson").item(0).getAttributes().getNamedItem("lessonId").getTextContent()); return Long.parseLong(document.getElementsByTagName("Lesson").item(0).getAttributes() .getNamedItem("lessonId").getNodeValue()); + } catch (MalformedURLException e) { throw new RuntimeException("Unable to start LAMS lesson, bad URL: '" + serverAddr + "', please check lams.properties", e); @@ -593,7 +586,6 @@ try { String method = "clone"; String timestamp = new Long(System.currentTimeMillis()).toString(); -// String username = "not_available";//signifier for skipping security check as we can't get the current user requested cloning String hash = generateAuthenticationHash(timestamp, username, serverId); String serviceURL = serverAddr + "/services/xml/LessonManager?" + "serverId=" + URLEncoder.encode(serverId, "utf8") + "&datetime=" + timestamp + "&username=" @@ -603,21 +595,14 @@ logger.info("LAMS clone lesson request: " + serviceURL); + // parse xml response and get the lesson id InputStream is = LamsSecurityUtil.callLamsServerPost(serviceURL); - // parse xml response DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document document = db.parse(is); - - // get the lesson id from the response - - /* - * The getTextContext is not a java 1.4 method, so Blackboard 7.1 comes up with errors using getNodeValue() - * instead - */ - // return Long.parseLong(document.getElementsByTagName("Lesson").item(0).getAttributes().getNamedItem("lessonId").getTextContent()); return Long.parseLong(document.getElementsByTagName("Lesson").item(0).getAttributes() .getNamedItem("lessonId").getNodeValue()); + } catch (MalformedURLException e) { throw new RuntimeException("Unable to clone LAMS lesson, bad URL: '" + serverAddr + "', please check lams.properties", e); @@ -649,6 +634,93 @@ } /** + * Import learning design in LAMS from its temp folder. Then starting a lesson using this learning design. + * + * @param courseId + * courseId as a request parameter + * @param ldId + * the learning design id for which you wish to start a lesson + * + * @return lesson id of a cloned lesson + * @throws LamsServerException + */ + public static Long importLearningDesign(User teacher, String courseId, String lsId, String ldId) throws LamsServerException { + + String serverId = getServerID(); + String serverAddr = getServerAddress(); + String serverKey = getServerKey(); + String username = teacher.getUserName(); + String locale = teacher.getLocale(); + String country = getCountry(locale); + String lang = getLanguage(locale); + + if (courseId == null || serverId == null || serverAddr == null || serverKey == null) { + logger.info("Unable to import lesson, one or more lams configuration properties or the course id is null"); + throw new RuntimeException("Unable to import lesson, one or more lams configuration properties or the course id is null. courseId="+courseId); + } + + //import a learning design + String filePath = EXPORT_FOLDER_LAMS_SERVER + lsId + "_" + ldId + ".zip"; + + try { + String filePathParam = URLEncoder.encode(filePath, "UTF-8"); + String timestamp = new Long(System.currentTimeMillis()).toString(); + String hash = generateAuthenticationHash(timestamp, username, serverId); + String serviceURL = serverAddr + "/services/xml/LessonManager?" + "serverId=" + + URLEncoder.encode(serverId, "utf8") + "&datetime=" + timestamp + "&username=" + + URLEncoder.encode(username, "utf8") + "&hashValue=" + hash + "&courseId=" + + URLEncoder.encode(courseId, "UTF8") + "&country=" + + country + "&lang=" + lang + "&method=import&customCSV=&filePath=" + filePathParam; + + logger.info("LAMS import lesson request: " + serviceURL); + + // parse xml response and get the ldid + InputStream is = LamsSecurityUtil.callLamsServerPost(serviceURL); + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document document = db.parse(is); + return Long.parseLong(document.getElementsByTagName("Lesson").item(0).getAttributes() + .getNamedItem("ldId").getNodeValue()); + + } catch (MalformedURLException e) { + throw new LamsServerException("Unable to import LAMS lesson, bad URL: '" + serverAddr + + "', please check lams.properties. Tried to import file " + filePath, e); + } catch (IllegalStateException e) { + throw new LamsServerException( + "LAMS Server timeout, did not get a response from the LAMS server. Please contact your systems administrator. Tried to import file " + + filePath, + e); + } catch (RemoteException e) { + throw new LamsServerException( + "Unable to import LAMS lesson, RMI Remote Exception. Tried to import file " + filePath, e); + } catch (UnsupportedEncodingException e) { + throw new LamsServerException( + "Unable to import LAMS lesson, Unsupported Encoding Exception. Tried to import file " + filePath, + e); + } catch (ConnectException e) { + throw new LamsServerException( + "LAMS Server timeout, did not get a response from the LAMS server. Please contact your systems administrator. Tried to import file " + + filePath, + e); + } catch (IOException e) { + throw new LamsServerException("Unable to import LAMS lesson. " + e.getMessage() + + " Please contact your system administrator. Tried to import file " + filePath, e); + } catch (ParserConfigurationException e) { + throw new LamsServerException("Unable to import LAMS lesson. " + e.getMessage() + + " Can't instantiate DocumentBuilder. Tried to import file " + filePath, e); + } catch (SAXException e) { + throw new LamsServerException("Unable to import LAMS lesson. " + e.getMessage() + + " Can't parse LAMS results. Tried to import file " + filePath, e); + } catch (Exception e) { + throw new LamsServerException( + "Unable to import LAMS lesson. Please contact your system administrator. Tried to import file " + + filePath, + e); + } + + } + + /** * Pre-adding students and monitors to a lesson * * @param ctx Index: lams_bb_integration/src/org/lamsfoundation/ld/util/LamsServerException.java =================================================================== diff -u --- lams_bb_integration/src/org/lamsfoundation/ld/util/LamsServerException.java (revision 0) +++ lams_bb_integration/src/org/lamsfoundation/ld/util/LamsServerException.java (revision b4a4a2a44c27e0307086d3405993f9febe39f13b) @@ -0,0 +1,34 @@ +package org.lamsfoundation.ld.util; + +/** + * Exception that originated at LAMS server. + */ +public class LamsServerException extends Exception { + + public LamsServerException() { + super(); + } + + /** + * @param message + */ + public LamsServerException(String message) { + super(message); + } + + /** + * @param cause + */ + public LamsServerException(Throwable cause) { + super(cause); + } + + /** + * @param message + * @param cause + */ + public LamsServerException(String message, Throwable cause) { + super(message, cause); + } + +} Index: lams_bb_integration/web/links/admin.jsp =================================================================== diff -u -r46f6e2a2d8c2b6552718747c373df53efd0cfc42 -rb4a4a2a44c27e0307086d3405993f9febe39f13b --- lams_bb_integration/web/links/admin.jsp (.../admin.jsp) (revision 46f6e2a2d8c2b6552718747c373df53efd0cfc42) +++ lams_bb_integration/web/links/admin.jsp (.../admin.jsp) (revision b4a4a2a44c27e0307086d3405993f9febe39f13b) @@ -47,8 +47,13 @@ <%-- Monitor Button --%>
+
+ +
@@ -61,22 +66,7 @@ // Open the LAMS Seuence Monitor Window function cloneLessons() { //block #buttons - $j('#buttons').block({ - message: '

Please, wait. Lessons are getting copied now.

', - baseZ: 1000000, - fadeIn: 0, - css: { - border: 'none', - padding: $j('#buttons').height() + 'px', - backgroundColor: '#000', - '-webkit-border-radius': '10px', - '-moz-border-radius': '10px', - opacity: .98 - }, - overlayCSS: { - opacity: 0 - } - }); + blockButtons(); $j.ajax({ async: true, @@ -98,6 +88,49 @@ return false; } + function importLessons() { + //block #buttons + blockButtons(); + + $j.ajax({ + async: true, + url: '../ImportLessons', + data : 'courseId=<%=ctx.getCourse().getCourseId()%>', + type: 'post', + success: function (response) { + $j("#buttons").unblock(); + alert(response); + }, + error: function (request, status, error) { + $j("#buttons").unblock(); + alert(error); + } + }); + + return false; + } + + //auxiliary method to block #buttons element + function blockButtons(){ + + $j('#buttons').block({ + message: '

Please, wait. Lessons are getting copied now.

', + baseZ: 1000000, + fadeIn: 0, + css: { + border: 'none', + padding: $j('#buttons').height() + 'px', + backgroundColor: '#000', + '-webkit-border-radius': '10px', + '-moz-border-radius': '10px', + opacity: .98 + }, + overlayCSS: { + opacity: 0 + } + }); + } +