diff --git a/LICENSE.txt b/LICENSE.txt index 70ff24253b..f8b9f039ee 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2008-2018 Regent8 of the University of California (Regents). Created +Copyright (c) 2008-2020 Regents of the University of California (Regents). Created by WISE, Graduate School of Education, University of California, Berkeley. This software is distributed under the GNU General Public License, v3, diff --git a/package-lock.json b/package-lock.json index 1f2060a327..11fdb45064 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "wise", - "version": "5.11.1", + "version": "5.12.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -5329,6 +5329,12 @@ "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, + "@types/dom-mediacapture-record": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.6.tgz", + "integrity": "sha512-N7+dsk8WPIvC59xZfbtvuTxfZI0I21uBLehhbz5Kx4Da9K26IdzqNuVX3ldWAxzYLVIsXr82MK9nlsKLxmka9A==", + "dev": true + }, "@types/events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", @@ -5885,6 +5891,11 @@ "resolved": "https://registry.npmjs.org/angularx-social-login/-/angularx-social-login-2.3.1.tgz", "integrity": "sha512-U0bf7ZdtB2oQicSMTzx9VEMEcW5gxo/Bb2dKWNTt5Tf0GANj89tf1rTF2YsSOHZbLPOdvtHXim+nARF7UCAjVg==" }, + "animation-frame-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/animation-frame-polyfill/-/animation-frame-polyfill-1.0.1.tgz", + "integrity": "sha1-X1rZk6eHlL0Xas3lttzmKGdBDJ0=" + }, "ansi-colors": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", @@ -6081,6 +6092,11 @@ "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", "dev": true }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=" + }, "array-initial": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", @@ -9251,11 +9267,16 @@ } }, "dom-autoscroller": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-autoscroller/-/dom-autoscroller-1.4.1.tgz", - "integrity": "sha1-jP2BX3biVsibp26xIlQbJ/GSL6Q=", - "requires": { - "create-point-cb": "^1.0.0" + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/dom-autoscroller/-/dom-autoscroller-2.3.4.tgz", + "integrity": "sha512-HcAdt/2Dq9x4CG6LWXc2x9Iq0MJPAu8fuzHncclq7byufqYEYVtx9sZ/dyzR+gdj4qwEC9p27Lw1G2HRRYX6jQ==", + "requires": { + "animation-frame-polyfill": "^1.0.0", + "create-point-cb": "^1.0.0", + "dom-mousemove-dispatcher": "^1.0.1", + "dom-plane": "^1.0.1", + "dom-set": "^1.0.1", + "type-func": "^1.0.1" } }, "dom-converter": { @@ -9267,6 +9288,19 @@ "utila": "~0.4" } }, + "dom-mousemove-dispatcher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dom-mousemove-dispatcher/-/dom-mousemove-dispatcher-1.0.1.tgz", + "integrity": "sha1-okpt35Oye7NpT3IIdUalf8fpFA8=" + }, + "dom-plane": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dom-plane/-/dom-plane-1.0.2.tgz", + "integrity": "sha1-+MheaXxYfxR+j8L6wd4HjB/kFyw=", + "requires": { + "create-point-cb": "^1.0.0" + } + }, "dom-serialize": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", @@ -9288,6 +9322,16 @@ "entities": "^1.1.1" } }, + "dom-set": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/dom-set/-/dom-set-1.1.1.tgz", + "integrity": "sha1-XCxhDuSDm1IO1fmN28vjFMD6lUo=", + "requires": { + "array-from": "^2.1.1", + "is-array": "^1.0.1", + "iselement": "^1.1.4" + } + }, "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", @@ -12770,6 +12814,11 @@ "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", "dev": true }, + "is-array": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-array/-/is-array-1.0.1.tgz", + "integrity": "sha1-6YUMwsyGDDvAl36EzPDdRkWEJ5o=" + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -13071,6 +13120,11 @@ "buffer-alloc": "^1.2.0" } }, + "iselement": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/iselement/-/iselement-1.1.4.tgz", + "integrity": "sha1-flW1Ko68pQp+LoDluNKEDzI1MUY=" + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", diff --git a/package.json b/package.json index 9424b73c5a..306b66e9fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wise", - "version": "5.12.0", + "version": "5.13.0", "description": "Web-based Inquiry Science Environment", "main": "app.js", "browserslist": [ @@ -48,7 +48,7 @@ "compute-covariance": "^1.0.1", "core-js": "^3.6.5", "cssnano": "^4.1.10", - "dom-autoscroller": "^1.3.1", + "dom-autoscroller": "^2.3.4", "eventemitter2": "^5.0.1", "fabric": "^3.6.3", "hammerjs": "^2.0.8", @@ -75,8 +75,8 @@ "zone.js": "~0.10.2" }, "devDependencies": { - "@angular-devkit/build-angular": "~0.901.3", "@angular-builders/custom-webpack": "^9.0.0", + "@angular-devkit/build-angular": "~0.901.3", "@angular/cli": "^9.1.3", "@angular/compiler-cli": "^9.0.0", "@angular/language-service": "^9.0.0", @@ -84,6 +84,7 @@ "@babel/core": "^7.9.0", "@babel/preset-env": "^7.9.5", "@locl/cli": "0.0.1-beta.7", + "@types/dom-mediacapture-record": "^1.0.6", "@types/jasmine": "^3.5.10", "@types/jasminewd2": "^2.0.8", "@types/jquery": "^3.3.38", @@ -140,7 +141,7 @@ "watch-sass": "npm rebuild node-sass && node ./node_modules/gulp/bin/gulp.js", "update-i18n": "node ./node_modules/gulp/bin/gulp.js update-i18n", "compile-sass": "node ./node_modules/gulp/bin/gulp.js compile-sass", - "test": "ng test wise --karma-config src/main/webapp/site/karma.conf.js --browsers ChromeHeadlessNoSandbox --watch=false", + "test": "ng test wise --source-map=false --karma-config src/main/webapp/site/karma.conf.js --browsers ChromeHeadlessNoSandbox --watch=false", "test-e2e-comment": "test-e2e assumes wise is already running.", "test-e2e": "node ./node_modules/protractor/bin/protractor src/main/webapp/wise5/test-e2e/conf.js", "locale-extractor": "./node_modules/.bin/ngx-extractor -i \"src/main/webapp/site/**/*.ts\" -o src/main/webapp/site/src/messages.xlf" diff --git a/pom.xml b/pom.xml index ff707cc5da..bd97cc8edd 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ wise war Web-based Inquiry Science Environment - 5.12.0 + 5.13.0 http://wise5.org diff --git a/src/main/java/org/wise/portal/dao/work/StudentWorkDao.java b/src/main/java/org/wise/portal/dao/work/StudentWorkDao.java index 6be4455871..6ce8e2c483 100644 --- a/src/main/java/org/wise/portal/dao/work/StudentWorkDao.java +++ b/src/main/java/org/wise/portal/dao/work/StudentWorkDao.java @@ -40,5 +40,5 @@ public interface StudentWorkDao extends SimpleDao { List getStudentWorkListByParams(Integer id, Run run, Group period, Workgroup workgroup, Boolean isAutoSave, Boolean isSubmit, String nodeId, String componentId, - String componentType, List components, Boolean latest); + String componentType, List components); } diff --git a/src/main/java/org/wise/portal/dao/work/impl/HibernateStudentWorkDao.java b/src/main/java/org/wise/portal/dao/work/impl/HibernateStudentWorkDao.java index 8507a664e8..9f78c4ba20 100644 --- a/src/main/java/org/wise/portal/dao/work/impl/HibernateStudentWorkDao.java +++ b/src/main/java/org/wise/portal/dao/work/impl/HibernateStudentWorkDao.java @@ -33,7 +33,6 @@ import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; -import javax.persistence.criteria.Subquery; import org.hibernate.Session; import org.json.JSONException; @@ -52,13 +51,13 @@ @Repository public class HibernateStudentWorkDao extends AbstractHibernateDao implements StudentWorkDao { - + @PersistenceContext private EntityManager entityManager; private CriteriaBuilder getCriteriaBuilder() { Session session = this.getHibernateTemplate().getSessionFactory().getCurrentSession(); - return session.getCriteriaBuilder(); + return session.getCriteriaBuilder(); } @Override @@ -74,34 +73,17 @@ protected Class getDataObjectClass() { @Override public List getStudentWorkListByParams(Integer id, Run run, Group period, Workgroup workgroup, Boolean isAutoSave, Boolean isSubmit, String nodeId, String componentId, - String componentType, List components, Boolean onlyGetLatest) { - if (Boolean.TRUE.equals(onlyGetLatest)) { - CriteriaBuilder cb = getCriteriaBuilder(); - CriteriaQuery cq = cb.createQuery(StudentWork.class); - Subquery subQuery = cq.subquery(Long.class); - Root subStudentWorkRoot = subQuery.from(StudentWork.class); - List predicates = getStudentWorkListByParamsPredicates(cb, subStudentWorkRoot, - id, run, period, workgroup, isAutoSave, isSubmit, nodeId, componentId, componentType, - components); - subQuery.select(cb.max(subStudentWorkRoot.get("id"))) - .where(predicates.toArray(new Predicate[predicates.size()])) - .groupBy(subStudentWorkRoot.get("workgroup")); - Root studentWorkRoot = cq.from(StudentWork.class); - cq.select(studentWorkRoot).where(cb.in(studentWorkRoot.get("id")).value(subQuery)); - TypedQuery query = entityManager.createQuery(cq); - return (List) query.getResultList(); - } else { - CriteriaBuilder cb = getCriteriaBuilder(); - CriteriaQuery cq = cb.createQuery(StudentWork.class); - Root studentWorkRoot = cq.from(StudentWork.class); - List predicates = getStudentWorkListByParamsPredicates(cb, studentWorkRoot, - id, run, period, workgroup, isAutoSave, isSubmit, nodeId, componentId, componentType, - components); - cq.select(studentWorkRoot).where(predicates.toArray(new Predicate[predicates.size()])) - .orderBy(cb.asc(studentWorkRoot.get("serverSaveTime"))); - TypedQuery query = entityManager.createQuery(cq); - return (List) query.getResultList(); - } + String componentType, List components) { + CriteriaBuilder cb = getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(StudentWork.class); + Root studentWorkRoot = cq.from(StudentWork.class); + List predicates = getStudentWorkListByParamsPredicates(cb, studentWorkRoot, + id, run, period, workgroup, isAutoSave, isSubmit, nodeId, componentId, componentType, + components); + cq.select(studentWorkRoot).where(predicates.toArray(new Predicate[predicates.size()])) + .orderBy(cb.asc(studentWorkRoot.get("serverSaveTime"))); + TypedQuery query = entityManager.createQuery(cq); + return (List) query.getResultList(); } private List getStudentWorkListByParamsPredicates(CriteriaBuilder cb, diff --git a/src/main/java/org/wise/portal/domain/workgroup/Workgroup.java b/src/main/java/org/wise/portal/domain/workgroup/Workgroup.java index e328b35501..db0446946e 100644 --- a/src/main/java/org/wise/portal/domain/workgroup/Workgroup.java +++ b/src/main/java/org/wise/portal/domain/workgroup/Workgroup.java @@ -83,6 +83,8 @@ public interface Workgroup extends Persistable { */ Long getId(); + void setId(Long id); + /** * Generates a name for this workgroup. * diff --git a/src/main/java/org/wise/portal/service/vle/wise5/impl/VLEServiceImpl.java b/src/main/java/org/wise/portal/service/vle/wise5/impl/VLEServiceImpl.java index 262a3d52bf..d5b2071d45 100644 --- a/src/main/java/org/wise/portal/service/vle/wise5/impl/VLEServiceImpl.java +++ b/src/main/java/org/wise/portal/service/vle/wise5/impl/VLEServiceImpl.java @@ -24,7 +24,9 @@ package org.wise.portal.service.vle.wise5.impl; import java.sql.Timestamp; +import java.util.ArrayList; import java.util.Calendar; +import java.util.HashMap; import java.util.List; import javax.transaction.Transactional; @@ -146,8 +148,30 @@ public List getStudentWorkList(Integer id, Integer runId, Integer p } } - return studentWorkDao.getStudentWorkListByParams(id, run, period, workgroup, isAutoSave, - isSubmit, nodeId, componentId, componentType, components, onlyGetLatest); + List studentWorkListByParams = studentWorkDao.getStudentWorkListByParams(id, run, + period, workgroup, isAutoSave, isSubmit, nodeId, componentId, componentType, components); + if (Boolean.TRUE.equals(onlyGetLatest)) { + return filterLatestWorkForEachWorkgroup(studentWorkListByParams); + } else { + return studentWorkListByParams; + } + } + + private List filterLatestWorkForEachWorkgroup( + List allStudentWork) { + HashMap latestWorkPerWorkgroup = new HashMap(); + for (StudentWork studentWork : allStudentWork) { + Long key = studentWork.getWorkgroup().getId(); + if (latestWorkPerWorkgroup.containsKey(key)) { + if (studentWork.getServerSaveTime().after( + latestWorkPerWorkgroup.get(key).getServerSaveTime())) { + latestWorkPerWorkgroup.put(key, studentWork); + } + } else { + latestWorkPerWorkgroup.put(studentWork.getWorkgroup().getId(), studentWork); + } + } + return new ArrayList(latestWorkPerWorkgroup.values()); } public JSONArray getNotebookItemsExport(Integer runId) { diff --git a/src/main/java/org/wise/vle/web/wise5/StudentAssetController.java b/src/main/java/org/wise/vle/web/wise5/StudentAssetController.java index 67af87f613..7a758e8291 100644 --- a/src/main/java/org/wise/vle/web/wise5/StudentAssetController.java +++ b/src/main/java/org/wise/vle/web/wise5/StudentAssetController.java @@ -33,13 +33,19 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import com.fasterxml.jackson.databind.node.ObjectNode; + import org.json.JSONArray; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest; import org.wise.portal.dao.ObjectNotFoundException; @@ -72,10 +78,7 @@ public class StudentAssetController { @Autowired private WorkgroupService workgroupService; - /** - * Returns student asset information based on specified parameters - */ - @RequestMapping(method = RequestMethod.GET, value = "/student/asset/{runId}") + @GetMapping("/student/asset/{runId}") protected void getStudentAssets( @PathVariable Integer runId, @RequestParam(value = "id", required = false) Integer id, @@ -143,7 +146,7 @@ protected void getStudentAssets( /** * Saves POSTed file into logged-in user's asset folder in the filesystem and in the database */ - @RequestMapping(method = RequestMethod.POST, value = "/student/asset/{runId}") + @PostMapping("/student/asset/{runId}") protected void postStudentAsset( @PathVariable Integer runId, @RequestParam(value = "periodId", required = true) Integer periodId, @@ -209,103 +212,69 @@ protected void postStudentAsset( } } - /** - * Removes specified asset from the filesystem and marks as deleted in the database - */ - @RequestMapping(method = RequestMethod.POST, value = "/student/asset/{runId}/remove") - protected void removeStudentAsset( - @PathVariable Integer runId, - @RequestParam(value = "studentAssetId", required = true) Integer studentAssetId, - @RequestParam(value = "workgroupId", required = true) Integer workgroupId, - @RequestParam(value = "clientDeleteTime", required = true) Long clientDeleteTime, - HttpServletResponse response) throws IOException { - User user = ControllerUtil.getSignedInUser(); - Run run = null; - try { - run = runService.retrieveById(new Long(runId)); - } catch (NumberFormatException e) { - e.printStackTrace(); - } catch (ObjectNotFoundException e) { - e.printStackTrace(); - } - - StudentAsset studentAsset = null; - try { - studentAsset = vleService.getStudentAssetById(studentAssetId); - } catch (ObjectNotFoundException e) { - e.printStackTrace(); - } + @PostMapping("/student/asset/{runId}/delete") + @ResponseBody + protected String removeStudentAsset(@PathVariable Integer runId, + @RequestBody ObjectNode postedParams) throws IOException, ObjectNotFoundException { + Run run = runService.retrieveById(new Long(runId)); + Integer studentAssetId = postedParams.get("studentAssetId").asInt(); + Integer workgroupId = postedParams.get("workgroupId").asInt(); + Long clientDeleteTime = postedParams.get("clientDeleteTime").asLong(); + StudentAsset studentAsset = vleService.getStudentAssetById(studentAssetId); String assetFileName = studentAsset.getFileName(); String dirName = run.getId() + "/" + workgroupId + "/unreferenced"; // looks like /studentuploads/[runId]/[workgroupId]/unreferenced String path = appProperties.getProperty("studentuploads_base_dir"); Boolean removeSuccess = AssetManager.removeAssetWISE5(path, dirName, assetFileName); if (removeSuccess) { studentAsset = vleService.deleteStudentAsset(studentAssetId, clientDeleteTime); - response.getWriter().write(studentAsset.toJSON().toString()); + return studentAsset.toJSON().toString(); } + return "error"; } - /** - * Copies specified asset in the filesystem and in the database - */ - @RequestMapping(method = RequestMethod.POST, value = "/student/asset/{runId}/copy") - protected void copyStudentAsset( - @PathVariable Integer runId, - @RequestParam(value = "studentAssetId", required = true) Integer studentAssetId, - @RequestParam(value = "periodId", required = true) Integer periodId, - @RequestParam(value = "workgroupId", required = true) Integer workgroupId, - @RequestParam(value = "nodeId", required = false) String nodeId, - @RequestParam(value = "componentId", required = false) String componentId, - @RequestParam(value = "componentType", required = false) String componentType, - @RequestParam(value = "clientSaveTime", required = true) String clientSaveTime, - HttpServletResponse response) throws IOException { - User user = ControllerUtil.getSignedInUser(); - Run run = null; - try { - run = runService.retrieveById(new Long(runId)); - } catch (NumberFormatException e) { - e.printStackTrace(); - } catch (ObjectNotFoundException e) { - e.printStackTrace(); - } - - StudentAsset studentAsset = null; - try { - studentAsset = vleService.getStudentAssetById(studentAssetId); - } catch (ObjectNotFoundException e) { - e.printStackTrace(); - } + @PostMapping("/student/asset/{runId}/copy") + @ResponseBody + protected String copyStudentAsset(@PathVariable Integer runId, + @RequestBody ObjectNode postedParams) throws IOException, ObjectNotFoundException { + Run run = runService.retrieveById(new Long(runId)); + Integer studentAssetId = postedParams.get("studentAssetId").asInt(); + Integer periodId = postedParams.get("periodId").asInt(); + Integer workgroupId = postedParams.get("workgroupId").asInt(); + String clientSaveTime = postedParams.get("clientSaveTime").asText(); + StudentAsset studentAsset = vleService.getStudentAssetById(studentAssetId); String assetFileName = studentAsset.getFileName(); String unreferencedDirName = run.getId() + "/" + workgroupId + "/unreferenced"; String referencedDirName = run.getId() + "/" + workgroupId + "/referenced"; - String copiedFileName = AssetManager.copyAssetForReferenceWISE5(unreferencedDirName, referencedDirName, assetFileName); + String copiedFileName = AssetManager.copyAssetForReferenceWISE5(unreferencedDirName, + referencedDirName, assetFileName); if (copiedFileName != null) { Integer id = null; Boolean isReferenced = true; String fileName = copiedFileName; String filePath = "/" + referencedDirName + "/" + copiedFileName; Long fileSize = studentAsset.getFileSize(); + String nodeId = null; + String componentId = null; + String componentType = null; String clientDeleteTime = null; - - StudentAsset copiedStudentAsset = null; try { - copiedStudentAsset = vleService.saveStudentAsset(id, runId, periodId, workgroupId, + StudentAsset copiedStudentAsset = vleService.saveStudentAsset(id, runId, periodId, workgroupId, nodeId, componentId, componentType, isReferenced, fileName, filePath, fileSize, clientSaveTime, clientDeleteTime); - response.getWriter().write(copiedStudentAsset.toJSON().toString()); + return copiedStudentAsset.toJSON().toString(); } catch (ObjectNotFoundException e) { e.printStackTrace(); - response.getWriter().write("error"); + return "error"; } } else { - response.getWriter().write("error"); + return "error"; } } /** * Returns size of logged-in student's unreferenced directory */ - @RequestMapping(method = RequestMethod.GET, value = "/student/asset/{runId}/size") + @GetMapping("/student/asset/{runId}/size") protected void getStudentAssetsSize(@PathVariable Long runId, HttpServletResponse response) throws IOException { User user = ControllerUtil.getSignedInUser(); diff --git a/src/main/resources/application_sample.properties b/src/main/resources/application_sample.properties index c249cf8c70..54f65f880f 100644 --- a/src/main/resources/application_sample.properties +++ b/src/main/resources/application_sample.properties @@ -157,14 +157,14 @@ runcode_prefixes_es=Cabra,Liebre,Oruga,Casa,Panda,Ciervo,Alce,Toro,Tigre,Rana,Sa # project_max_total_assets_size: max size for all assets combined uploaded for each project, in bytes. Default: 20MB=20971520 bytes. For reference: 10MB=10485760 bytes, 15MB=15728640 bytes, 50MB=52428800 bytes # remember to set maxFileUploadSize >= project_max_total_assets_size # student_max_asset_size: max size for any asset uploaded by student, in bytes. Default: 5MB=5242880 bytes -# student_max_total_assets_size: max size for all assets combined uploaded by student, in bytes. Default: 2MB=2097152 bytes +# student_max_total_assets_size: max size for all assets combined uploaded by student, in bytes. Default: 10MB=10485760 bytes # # Note: if you set any of these values above 1MB, you'll also need to set mysql's max_allowed_packet value to allow saving the big data. # See this post: https://groups.google.com/d/topic/wise4-dev/CPS4AZEiquo/discussion project_max_total_assets_size=20971520 student_max_asset_size=5242880 -student_max_total_assets_size=2097152 +student_max_total_assets_size=10485760 # allowed assets for projects. Reference: http://en.wikipedia.org/wiki/Internet_media_type normalAuthorAllowedProjectAssetContentTypes=text/plain,text/csv,text/xml,application/pdf,image/gif,image/jpeg,image/png,image/svg+xml,image/gif,audio/mp3,audio/mp4,audio/mpeg,audio/wav,audio/vnd.wave,audio/ogg,audio/webm,audio/x-aac,video/mpeg,video/mp4,video/ogg,video/quicktime,video/x-flv,video/avi,video/webm diff --git a/src/main/resources/version.txt b/src/main/resources/version.txt index dd0ad7ae60..26f30f79cc 100644 --- a/src/main/resources/version.txt +++ b/src/main/resources/version.txt @@ -1 +1 @@ -5.12.0 +5.13.0 diff --git a/src/main/webapp/site/src/app/home/home.component.html b/src/main/webapp/site/src/app/home/home.component.html index 1be803c5f6..f7042f033c 100644 --- a/src/main/webapp/site/src/app/home/home.component.html +++ b/src/main/webapp/site/src/app/home/home.component.html @@ -1,5 +1,6 @@
- +
= [ { imgSrc: 'assets/img/wise-students-building@2x.jpg', diff --git a/src/main/webapp/site/src/app/modules/shared/hero-section/hero-section.component.html b/src/main/webapp/site/src/app/modules/shared/hero-section/hero-section.component.html index 4855382f60..5f7f5b343d 100644 --- a/src/main/webapp/site/src/app/modules/shared/hero-section/hero-section.component.html +++ b/src/main/webapp/site/src/app/modules/shared/hero-section/hero-section.component.html @@ -1,5 +1,12 @@
+ + + + + {{ imgDescription }} +
{{ headline }}

; + @ContentChild('headlineTemplate', { static: false }) headlineRef: TemplateRef; @Input() tagline: string; - @ContentChild('taglineTemplate', {static:false}) taglineRef: TemplateRef; + @ContentChild('taglineTemplate', { static: false }) taglineRef: TemplateRef; + + @ViewChild('bgRef') bgRef: ElementRef; bgStyle: SafeStyle; @@ -28,8 +36,10 @@ export class HeroSectionComponent implements OnInit { this.sanitizer = sanitizer; } - ngOnInit() { - this.bgStyle = this.getBgStyle(); + ngAfterViewInit() { + this.bgRef.nativeElement.onload = () => { + this.bgStyle = this.getBgStyle(); + } } /** @@ -37,7 +47,7 @@ export class HeroSectionComponent implements OnInit { * @returns {SafeStyle} */ getBgStyle(): SafeStyle { - const STYLE = `url(${this.imgSrc})`; - return this.sanitizer.bypassSecurityTrustStyle(STYLE); + const style: string = `url(${this.bgRef.nativeElement.currentSrc})`; + return this.sanitizer.bypassSecurityTrustStyle(style); } } diff --git a/src/main/webapp/site/src/app/preview/preview-angular-js-module.ts b/src/main/webapp/site/src/app/preview/preview-angular-js-module.ts index a1b90b68bc..c5a6dd2db9 100644 --- a/src/main/webapp/site/src/app/preview/preview-angular-js-module.ts +++ b/src/main/webapp/site/src/app/preview/preview-angular-js-module.ts @@ -9,6 +9,9 @@ import { UtilService } from '../../../../wise5/services/utilService'; import { ConfigService } from '../../../../wise5/services/configService'; import { ProjectService } from '../../../../wise5/services/projectService'; import { VLEProjectService } from '../../../../wise5/vle/vleProjectService'; +import { CRaterService } from '../../../../wise5/services/cRaterService'; +import { SessionService } from '../../../../wise5/services/sessionService'; +import { StudentAssetService } from '../../../../wise5/services/studentAssetService'; @Component({template: ``}) export class EmptyComponent {} @@ -26,7 +29,10 @@ export class EmptyComponent {} providers: [ UtilService, ConfigService, + CRaterService, ProjectService, + SessionService, + StudentAssetService, VLEProjectService ] }) diff --git a/src/main/webapp/site/src/app/services/cRaterService.spec.ts b/src/main/webapp/site/src/app/services/cRaterService.spec.ts new file mode 100644 index 0000000000..9d5e7c060f --- /dev/null +++ b/src/main/webapp/site/src/app/services/cRaterService.spec.ts @@ -0,0 +1,410 @@ +import { TestBed } from '@angular/core/testing'; +import { CRaterService } from '../../../../wise5/services/cRaterService'; +import { UpgradeModule } from '@angular/upgrade/static'; +import ConfigService from '../../../../wise5/services/configService'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +let service: CRaterService; +let configService: ConfigService; +let http: HttpTestingController; + +describe('CRaterService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ HttpClientTestingModule, UpgradeModule ], + providers: [ ConfigService, CRaterService ] + }); + http = TestBed.get(HttpTestingController); + configService = TestBed.get(ConfigService); + service = TestBed.get(CRaterService); + }); + + makeCRaterScoringRequest(); + getCRaterItemType(); + getCRaterItemId(); + getCRaterScoreOn(); + isCRaterEnabled(); + isCRaterScoreOnSave(); + isCRaterScoreOnSubmit(); + isCRaterScoreOnChange(); + getCRaterScoringRuleByScore(); + getCRaterFeedbackTextByScore(); + getMultipleAttemptCRaterFeedbackTextByScore(); + getMultipleAttemptCRaterScoringRuleByScore(); + makeCRaterVerifyRequest(); +}); + +function makeCRaterScoringRequest() { + describe('makeCRaterScoringRequest()', () => { + it('should make a CRater scoring request', () => { + spyOn(configService, 'getCRaterRequestURL').and.returnValue('/c-rater') + const itemId = 'ColdBeverage1Sub'; + const responseId = 1; + const studentData = 'Hello World.'; + service.makeCRaterScoringRequest(itemId, responseId, studentData); + http.expectOne({ + url: `/c-rater/score?itemId=${itemId}&responseId=${responseId}` + + `&studentData=${encodeURI(studentData)}`, + method: 'GET' + }); + }); + }); +} + +function getCRaterItemType() { + describe('getCRaterItemType()', () => { + it('should get the CRater item type', () => { + const itemType = 'CRATER'; + const component = { + cRater: { + itemType: itemType + } + }; + expect(service.getCRaterItemType(component)).toEqual(itemType); + }); + }); +} + +function getCRaterItemId() { + describe('getCRaterItemId()', () => { + it('should get the CRater Id', () => { + const itemId = 'ColdBeverage1Sub'; + const component = { + cRater: { + itemId: itemId + } + }; + expect(service.getCRaterItemId(component)).toEqual(itemId); + }); + }); +} + +function getCRaterScoreOn() { + describe('getCRaterScoreOn()', () => { + it('should get the CRater score on submit', () => { + const scoreOn = 'submit'; + const component = { + enableCRater: true, + cRater: { + scoreOn: scoreOn + } + }; + expect(service.getCRaterScoreOn(component)).toEqual(scoreOn); + }); + + it('should get the CRater score on save', () => { + const scoreOn = 'save'; + const component = { + enableCRater: true, + cRater: { + scoreOn: scoreOn + } + }; + expect(service.getCRaterScoreOn(component)).toEqual(scoreOn); + }); + + it('should get the CRater score on change', () => { + const scoreOn = 'change'; + const component = { + enableCRater: true, + cRater: { + scoreOn: scoreOn + } + }; + expect(service.getCRaterScoreOn(component)).toEqual(scoreOn); + }); + }); +} + +function isCRaterEnabled() { + describe('isCRaterEnabled()', () => { + it('should check if CRater is enabled when true', () => { + const component = { + enableCRater: true, + cRater: { + itemType: 'CRATER', + itemId: 'ColdBeverage1Sub' + } + }; + expect(service.isCRaterEnabled(component)).toEqual(true); + }); + + it('should check if CRater is enabled when false', () => { + const component = { + enableCRater: false, + cRater: { + itemType: 'CRATER', + itemId: 'ColdBeverage1Sub' + } + }; + expect(service.isCRaterEnabled(component)).toEqual(false); + }); + }); +} + +function isCRaterScoreOnSave() { + describe('isCRaterScoreOnSave()', () => { + it('should get is CRater score on save when true', () => { + const scoreOn = 'save'; + const component = { + cRater: { + scoreOn: scoreOn + } + }; + expect(service.isCRaterScoreOnSave(component)).toEqual(true); + }); + + it('should get is CRater score on save when false', () => { + const scoreOn = 'submit'; + const component = { + cRater: { + scoreOn: scoreOn + } + }; + expect(service.isCRaterScoreOnSave(component)).toEqual(false); + }); + }); +} + +function isCRaterScoreOnSubmit() { + describe('isCRaterScoreOnSubmit()', () => { + it('should get is CRater score on submit when true', () => { + const scoreOn = 'submit'; + const component = { + cRater: { + scoreOn: scoreOn + } + }; + expect(service.isCRaterScoreOnSubmit(component)).toEqual(true); + }); + + it('should get is CRater score on submit when false', () => { + const scoreOn = 'save'; + const component = { + cRater: { + scoreOn: scoreOn + } + }; + expect(service.isCRaterScoreOnSubmit(component)).toEqual(false); + }); + }); +} + +function isCRaterScoreOnChange() { + describe('isCRaterScoreOnChange()', () => { + it('should get is CRater score on change when true', () => { + const scoreOn = 'change'; + const component = { + cRater: { + scoreOn: scoreOn + } + }; + expect(service.isCRaterScoreOnChange(component)).toEqual(true); + }); + + it('should get is CRater score on change when false', () => { + const scoreOn = 'submit'; + const component = { + cRater: { + scoreOn: scoreOn + } + }; + expect(service.isCRaterScoreOnChange(component)).toEqual(false); + }); + }); +} + +function createScoringRule(score, feedbackText) { + return { + score: score, + feedbackText: feedbackText + } +} + +function getCRaterScoringRuleByScore() { + describe('getCRaterScoringRuleByScore()', () => { + it('should get CRater scoring rule by score 1', () => { + const scoringRule1 = createScoringRule(1, 'You received a score of 1.'); + const scoringRule2 = createScoringRule(2, 'You received a score of 2.'); + const component = { + cRater: { + scoringRules: [ + scoringRule1, + scoringRule2 + ] + } + }; + expect(service.getCRaterScoringRuleByScore(component, 1)).toEqual(scoringRule1); + }); + + it('should get CRater scoring rule by score 2', () => { + const scoringRule1 = createScoringRule(1, 'You received a score of 1.'); + const scoringRule2 = createScoringRule(2, 'You received a score of 2.'); + const component = { + cRater: { + scoringRules: [ + scoringRule1, + scoringRule2 + ] + } + }; + expect(service.getCRaterScoringRuleByScore(component, 2)).toEqual(scoringRule2); + }); + }); +} + +function getCRaterFeedbackTextByScore() { + describe('getCRaterFeedbackTextByScore()', () => { + it('should get CRater feedback text by score 1', () => { + const feedbackText = 'You received a score of 1.'; + const scoringRule1 = createScoringRule(1, feedbackText); + const scoringRule2 = createScoringRule(2, 'You received a score of 2.'); + const component = { + cRater: { + scoringRules: [ + scoringRule1, + scoringRule2 + ] + } + }; + expect(service.getCRaterFeedbackTextByScore(component, 1)).toEqual(feedbackText); + }); + + it('should get CRater feedback text by score 2', () => { + const feedbackText = 'You received a score of 2.'; + const scoringRule1 = createScoringRule(1, 'You received a score of 1.'); + const scoringRule2 = createScoringRule(2, feedbackText); + const component = { + cRater: { + scoringRules: [ + scoringRule1, + scoringRule2 + ] + } + }; + expect(service.getCRaterFeedbackTextByScore(component, 2)).toEqual(feedbackText); + }); + }); +} + +function getMultipleAttemptCRaterFeedbackTextByScore() { + describe('getMultipleAttemptCRaterFeedbackTextByScore()', () => { + it('should get multiple attempt CRater feedback text by score 1 then 2', () => { + const feedbackText = 'You improved a little.'; + const component = { + cRater: { + multipleAttemptScoringRules: [ + { + scoreSequence: [1, 2], + feedbackText: feedbackText + }, + { + scoreSequence: [2, 1], + feedbackText: 'You got worse.' + } + ] + } + }; + expect(service.getMultipleAttemptCRaterFeedbackTextByScore(component, 1, 2)) + .toEqual(feedbackText); + }); + + it('should get multiple attempt CRater feedback text by score 2 then 1', () => { + const feedbackText = 'You got worse.'; + const component = { + cRater: { + multipleAttemptScoringRules: [ + { + scoreSequence: [1, 2], + feedbackText: 'You improved a little.' + }, + { + scoreSequence: [2, 1], + feedbackText: feedbackText + } + ] + } + }; + expect(service.getMultipleAttemptCRaterFeedbackTextByScore(component, 2, 1)) + .toEqual(feedbackText); + }); + }); +} + +function getMultipleAttemptCRaterScoringRuleByScore() { + it('should get multiple attempt CRater scoring rule by specific score', () => { + const multipleAttemptScoringRule1To2 = { + scoreSequence: [1, 2], + feedbackText: 'You improved a little.' + }; + const multipleAttemptScoringRule2To1 = { + scoreSequence: [2, 1], + feedbackText: 'You got worse.' + }; + const component = { + cRater: { + multipleAttemptScoringRules: [ + multipleAttemptScoringRule1To2, + multipleAttemptScoringRule2To1 + ] + } + }; + expect(service.getMultipleAttemptCRaterScoringRuleByScore(component, 1, 2)) + .toEqual(multipleAttemptScoringRule1To2); + }); + + it('should get multiple attempt CRater scoring rule by score with range', () => { + const multipleAttemptScoringRule1To45 = { + scoreSequence: [1, "4-5"], + feedbackText: 'You improved a lot.' + }; + const multipleAttemptScoringRule2To1 = { + scoreSequence: [2, 1], + feedbackText: 'You got worse.' + }; + const component = { + cRater: { + multipleAttemptScoringRules: [ + multipleAttemptScoringRule1To45, + multipleAttemptScoringRule2To1 + ] + } + }; + expect(service.getMultipleAttemptCRaterScoringRuleByScore(component, 1, 5)) + .toEqual(multipleAttemptScoringRule1To45); + }); + + it('should get multiple attempt CRater scoring rule by score with comma separated values', () => { + const multipleAttemptScoringRule1To345 = { + scoreSequence: [1, "3,4,5"], + feedbackText: 'You improved a lot.' + }; + const multipleAttemptScoringRule2To1 = { + scoreSequence: [2, 1], + feedbackText: 'You got worse.' + }; + const component = { + cRater: { + multipleAttemptScoringRules: [ + multipleAttemptScoringRule1To345, + multipleAttemptScoringRule2To1 + ] + } + }; + expect(service.getMultipleAttemptCRaterScoringRuleByScore(component, 1, 4)) + .toEqual(multipleAttemptScoringRule1To345); + }); +} + +function makeCRaterVerifyRequest() { + describe('makeCRaterVerifyRequest()', () => { + it('should make a CRater verify request', () => { + spyOn(configService, 'getCRaterRequestURL').and.returnValue('/c-rater') + const itemId = 'ColdBeverage1Sub'; + service.makeCRaterVerifyRequest(itemId); + http.expectOne({ + url: `/c-rater/verify?itemId=${itemId}`, + method: 'GET' + }); + }); + }); +} \ No newline at end of file diff --git a/src/main/webapp/site/src/app/services/projectService.spec.ts b/src/main/webapp/site/src/app/services/projectService.spec.ts index f34bf6bcb4..9ac48d0148 100644 --- a/src/main/webapp/site/src/app/services/projectService.spec.ts +++ b/src/main/webapp/site/src/app/services/projectService.spec.ts @@ -25,10 +25,10 @@ describe('ProjectService', () => { providers: [ ProjectService, ConfigService, UtilService ] }); http = TestBed.get(HttpTestingController); - service = TestBed.get(ProjectService); configService = TestBed.get(ConfigService); utilService = TestBed.get(UtilService); - spyOn(utilService, 'broadcastEventInRootScope'); + spyOn(utilService, 'broadcastEventInRootScope').and.callFake(() => {}); + service = TestBed.get(ProjectService); demoProjectJSON = JSON.parse(JSON.stringify(demoProjectJSON_import)); scootersProjectJSON = JSON.parse(JSON.stringify(scootersProjectJSON_import)); }); @@ -71,18 +71,23 @@ describe('ProjectService', () => { shouldBeAbleToInsertAStepNodeAfterAnotherStepNode(); shouldBeAbleToInsertAnActivityNodeAfterAnotherActivityNode(); shouldNotBeAbleToInsertANodeAfterAnotherNodeWhenTheyAreDifferentTypes(); - shouldBeAbleToInsertAStepNodeInsideAnGroupNode(); + shouldBeAbleToInsertAStepNodeInsideAGroupNode(); shouldBeAbleToInsertAGroupNodeInsideAGroupNode(); shouldNotBeAbleToInsertAStepNodeInsideAStepNode(); shouldDeleteAStepFromTheProject(); shouldDeleteAnInactiveStepFromTheProject(); shouldDeleteAStepThatIsTheStartIdOfTheProject(); + shouldDeleteAStepThatIsTheLastStepOfTheProject(); shouldDeleteAStepThatIsTheStartIdOfAnAactivityThatIsNotTheFirstActivity(); shouldDeleteTheFirstActivityFromTheProject(); - shouldDeleteAnActivityThatIsNotTheFirstFromTheProject(); + shouldDeleteAnActivityInTheMiddleOfTheProject(); + shouldDeleteTheLastActivityFromTheProject(); calculateNodeOrder(); getGroupNodesIdToOrder(); getUniqueAuthors(); + deleteActivityWithBranching(); + deleteTheLastStepInAnActivity(); + deleteAllStepsInAnActivity(); // TODO: add test for service.getFlattenedProjectAsNodeIds() // TODO: add test for service.getAllPaths() // TODO: add test for service.consolidatePaths() @@ -190,7 +195,7 @@ function shouldRetrieveProjectWhenConfigProjectURLIsValid() { service.retrieveProject().then((response) => { expect(response).toEqual(scootersProjectJSON); }); - http.expectOne(projectURL).flush(scootersProjectJSON); + http.expectOne(projectURL); }); } @@ -215,7 +220,7 @@ function shouldSaveProject() { service.setProject(scootersProjectJSON); service.saveProject(); expect(configService.getConfigParam).toHaveBeenCalledWith('saveProjectURL'); - http.expectOne(saveProjectURL).flush({status: 'success'}); + http.expectOne(saveProjectURL); }); } @@ -713,7 +718,7 @@ function shouldNotBeAbleToInsertANodeAfterAnotherNodeWhenTheyAreDifferentTypes() }); } -function shouldBeAbleToInsertAStepNodeInsideAnGroupNode() { +function shouldBeAbleToInsertAStepNodeInsideAGroupNode() { it('should be able to insert a step node inside an group node', () => { service.setProject(demoProjectJSON); const node1 = service.getNodeById('node1'); @@ -759,7 +764,7 @@ function shouldNotBeAbleToInsertAStepNodeInsideAStepNode() { function shouldDeleteAStepFromTheProject() { it('should delete a step from the project', () => { service.setProject(demoProjectJSON); - expect(service.getNodes().length).toEqual(37); + expect(service.getNodes().length).toEqual(48); expect(service.getNodeById('node5')).not.toBeNull(); expect( service.nodeHasTransitionToNodeId(service.getNodeById('node4'), 'node5') @@ -769,7 +774,7 @@ function shouldDeleteAStepFromTheProject() { ).toBeTruthy(); expect(service.getNodesWithTransitionToNodeId('node6').length).toEqual(1); service.deleteNode('node5'); - expect(service.getNodes().length).toEqual(36); + expect(service.getNodes().length).toEqual(47); expect(service.getNodeById('node5')).toBeNull(); expect( service.nodeHasTransitionToNodeId(service.getNodeById('node4'), 'node6') @@ -800,8 +805,24 @@ function shouldDeleteAStepThatIsTheStartIdOfTheProject() { }); } +function shouldDeleteAStepThatIsTheLastStepOfTheProject() { + it('should delete a step that is the last step of the project', () => { + service.setProject(demoProjectJSON); + expect(service.getTransitionsByFromNodeId('node797').length).toEqual(1); + expect( + service.nodeHasTransitionToNodeId(service.getNodeById('node797'), 'node798') + ).toBeTruthy(); + service.deleteNode('node798'); + expect(service.getTransitionsByFromNodeId('node797').length).toEqual(0); + expect( + service.nodeHasTransitionToNodeId(service.getNodeById('node797'), 'node798') + ).toBeFalsy(); + }); +} + function shouldDeleteAStepThatIsTheStartIdOfAnAactivityThatIsNotTheFirstActivity() { - it('should delete a step that is the start id of an activity that is not the first activity', () => { + it('should delete a step that is the start id of an activity that is not the first activity', + () => { service.setProject(demoProjectJSON); expect(service.getGroupStartId('group2')).toEqual('node20'); expect( @@ -823,31 +844,49 @@ function shouldDeleteTheFirstActivityFromTheProject() { service.setProject(demoProjectJSON); expect(service.getGroupStartId('group0')).toEqual('group1'); expect(service.getStartNodeId()).toEqual('node1'); - expect(service.getNodes().length).toEqual(37); + expect(service.getNodes().length).toEqual(48); expect(service.getNodesWithTransitionToNodeId('node20').length).toEqual(1); service.deleteNode('group1'); expect(service.getNodeById('group1')).toBeNull(); expect(service.getGroupStartId('group0')).toEqual('group2'); expect(service.getStartNodeId()).toEqual('node20'); - expect(service.getNodes().length).toEqual(17); + expect(service.getNodes().length).toEqual(28); expect(service.getNodesWithTransitionToNodeId('node20').length).toEqual(0); }); } -function shouldDeleteAnActivityThatIsNotTheFirstFromTheProject() { - it('should delete an activity that is not the first from the project', () => { +function shouldDeleteAnActivityInTheMiddleOfTheProject() { + it('should delete an activity that is in the middle of the project', () => { service.setProject(demoProjectJSON); expect( - service.nodeHasTransitionToNodeId(service.getNodeById('group1'), 'group2') + service.nodeHasTransitionToNodeId(service.getNodeById('group2'), 'group3') ).toBeTruthy(); - expect(service.getTransitionsByFromNodeId('group1').length).toEqual(1); - expect(service.getNodes().length).toEqual(37); - service.deleteNode('group2'); + expect(service.getNodes().length).toEqual(48); + service.deleteNode('group3'); expect( - service.nodeHasTransitionToNodeId(service.getNodeById('group1'), 'group2') + service.nodeHasTransitionToNodeId(service.getNodeById('group2'), 'group3') ).toBeFalsy(); - expect(service.getTransitionsByFromNodeId('group1').length).toEqual(0); - expect(service.getNodes().length).toEqual(21); + expect( + service.nodeHasTransitionToNodeId(service.getNodeById('group2'), 'group4') + ).toBeTruthy(); + expect(service.getNodes().length).toEqual(45); + }); +} + +function shouldDeleteTheLastActivityFromTheProject() { + it('should delete the last activity from the project', () => { + service.setProject(demoProjectJSON); + expect( + service.nodeHasTransitionToNodeId(service.getNodeById('group3'), 'group4') + ).toBeTruthy(); + expect(service.getTransitionsByFromNodeId('group3').length).toEqual(1); + expect(service.getNodes().length).toEqual(48); + service.deleteNode('group4'); + expect( + service.nodeHasTransitionToNodeId(service.getNodeById('group3'), 'group4') + ).toBeFalsy(); + expect(service.getTransitionsByFromNodeId('group3').length).toEqual(0); + expect(service.getNodes().length).toEqual(40); }); } @@ -878,7 +917,9 @@ function getGroupNodesIdToOrder() { expect(service.getGroupNodesIdToOrder()).toEqual({ group0: { order: 0 }, group1: { order: 1 }, - group2: { order: 21 } + group2: { order: 21 }, + group3: { order: 37 }, + group4: { order: 40 } }); }); }); @@ -945,3 +986,68 @@ function getUniqueAuthors() { }); }); } + +function deleteActivityWithBranching() { + it(`should delete an activity with branching and is also the first activity in the project + and properly set the project start node id`, () => { + service.setProject(demoProjectJSON); + expect(service.getStartNodeId()).toEqual('node1'); + service.deleteNode('group1'); + expect(service.getStartNodeId()).toEqual('node20'); + }); + + it(`should delete an activity in the middle of the project with branching and properly remove + transitions from remaining steps`, () => { + service.setProject(demoProjectJSON); + const node19 = service.getNodeById('node19'); + const node19Transitions = node19.transitionLogic.transitions; + expect(node19Transitions.length).toEqual(1); + expect(node19Transitions[0].to).toEqual('node20'); + service.deleteNode('group2'); + expect(node19Transitions.length).toEqual(1); + expect(node19Transitions[0].to).toEqual('node790'); + }); + + it(`should delete an activity at the end of the project with branching and properly remove + transitions from remaining steps`, () => { + service.setProject(demoProjectJSON); + const node791 = service.getNodeById('node791'); + const node791Transitions = node791.transitionLogic.transitions; + expect(node791Transitions.length).toEqual(1); + expect(node791Transitions[0].to).toEqual('node792'); + service.deleteNode('group4'); + expect(node791Transitions.length).toEqual(0); + }); +} + +function deleteTheLastStepInAnActivity() { + it(`should delete the last step in an activity in the middle of the project and set previous + step to transition to the first step of the next activity`, () => { + service.setProject(demoProjectJSON); + const node790Transitions = service.getTransitionsByFromNodeId('node790'); + expect(node790Transitions.length).toEqual(1); + expect( + service.nodeHasTransitionToNodeId(service.getNodeById('node790'), 'node791') + ).toBeTruthy(); + service.deleteNode('node791'); + expect(node790Transitions.length).toEqual(1); + expect( + service.nodeHasTransitionToNodeId(service.getNodeById('node790'), 'node792') + ).toBeTruthy(); + }); +} + +function deleteAllStepsInAnActivity() { + it(`should delete all steps in an activity in the middle of the project and set previous step + to transition to activity`, () => { + service.setProject(demoProjectJSON); + const node34 = service.getNodeById('node34'); + const node34Transitions = node34.transitionLogic.transitions; + expect(node34Transitions.length).toEqual(1); + expect(node34Transitions[0].to).toEqual('node790'); + service.deleteNode('node790'); + service.deleteNode('node791'); + expect(node34Transitions.length).toEqual(1); + expect(node34Transitions[0].to).toEqual('group3'); + }); +} \ No newline at end of file diff --git a/src/main/webapp/site/src/app/services/sampleData/curriculum/Demo.project.json b/src/main/webapp/site/src/app/services/sampleData/curriculum/Demo.project.json index 8260dd8d7c..cd3a9426c3 100644 --- a/src/main/webapp/site/src/app/services/sampleData/curriculum/Demo.project.json +++ b/src/main/webapp/site/src/app/services/sampleData/curriculum/Demo.project.json @@ -1,1780 +1,2515 @@ { "nodes": [ - { - "id": "group0", - "type": "group", - "title": "Master", - "startId": "group1", - "ids": [ - "group1", - "group2" - ], - "transitionLogic": { - "transitions": [ - { - "to": "group1" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null - } - }, - { - "id": "group1", - "type": "group", - "title": "Example Steps", - "startId": "node1", - "ids": [ - "node1", - "node2", - "node3", - "node4", - "node5", - "node6", - "node7", - "node8", - "node9", - "node10", - "node11", - "node12", - "node13", - "node14", - "node15", - "node16", - "node17", - "node18", - "node19" - ], - "transitionLogic": { - "transitions": [ - { - "to": "group2" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null - } - }, - { - "id": "node1", - "type": "node", - "showSaveButton": false, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node2" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null - }, - "title": "HTML Step", - "components": [ - { - "id": "zh4h1urdys", - "type": "HTML", - "html": "\n\nhtml\n\n\n

This is a step where authors can enter their own html. Authors can also embed images and videos.

\n

\"\"

\n

\n\n" - } - ] - }, - { - "id": "node2", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node3" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null - }, - "title": "Open Response Step", - "components": [ - { - "id": "7edwu1p29b", - "type": "OpenResponse", - "prompt": "

This is a step where students enter text.

" - } - ] - }, - { - "id": "node3", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node4" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null - }, - "title": "Open Response Step Auto Graded", - "components": [ - { - "id": "0sef5ya2wj", - "type": "OpenResponse", - "prompt": "

This is an auto-graded step that will give the student an automated score and automated text feedback. (Note: the auto graded functionality only works in a run and does not work when simply previewing the project)

\n

Explain how the sun helps animals survive.

" - } - ] - }, - { - "id": "node4", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node5" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null - }, - "title": "Note Step", - "components": [ - { - "id": "9rfz8yh32s", - "type": "OpenResponse", - "prompt": "

This is similar to an open response step but the text entry area opens in a popup so that students can still see the content from the previous step.

" - } - ] - }, - { - "id": "node5", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node6" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null - }, - "title": "Multiple Choice Step Single Answer", - "components": [ - { - "id": "09hahe7wsm", - "type": "MultipleChoice", - "prompt": "

This is a multiple choice step where the student is allowed to choose one choice.

\n

Who lives in a pineapple under the sea?

", - "choices": [ - { - "id": "y6rvd7eziz", - "text": "Spongebob", - "feedback": "Great Job!", - "isCorrect": true - }, - { - "id": "ti5rd0es02", - "text": "Patrick", - "feedback": "Patrick doesn't live in a pineapple, he lives under a rock", - "isCorrect": false - }, - { - "id": "gb0cnkaiem", - "text": "Squidward", - "feedback": "Squidward doesn't live in a pineapple, he lives in an Easter island head", - "isCorrect": false - } + { + "id": "group0", + "type": "group", + "title": "Master", + "startId": "group1", + "ids": [ + "group1", + "group2", + "group3", + "group4" ], - "showSaveButton": false, - "showSubmitButton": true, - "choiceType": "radio" - } - ] - }, - { - "id": "node6", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node7" + "transitionLogic": { + "transitions": [ + { + "to": "group1" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null }, - "title": "Multiple Choice Step Multiple Answer", - "components": [ - { - "id": "ajsdwjugeb", - "type": "MultipleChoice", - "prompt": "

This is a multiple choice step where the student is allowed to choose multiple choices.

\n

Which of these are Ninja Turtles?

", - "choices": [ - { - "id": "ofjky47v57", - "text": "Leonardo", - "feedback": "Great Job you know your Ninja Turtles", - "isCorrect": true - }, - { - "id": "dvsbhlfemx", - "text": "Donatello", - "feedback": "Great Job you know your Ninja Turtles", - "isCorrect": true - }, - { - "id": "i8et2px6m3", - "text": "Michelangelo", - "feedback": "Great Job you know your Ninja Turtles", - "isCorrect": true - }, - { - "id": "60gxmv4c76", - "text": "Raphael", - "feedback": "Great Job you know your Ninja Turtles", - "isCorrect": true - }, - { - "id": "2oxfbn09lk", - "text": "Squirtle", - "feedback": "Squirtle is a Pokemon", - "isCorrect": false - } + { + "id": "group1", + "type": "group", + "title": "Example Steps", + "startId": "node1", + "ids": [ + "node1", + "node2", + "node3", + "node4", + "node5", + "node6", + "node7", + "node8", + "node9", + "node10", + "node11", + "node12", + "node13", + "node14", + "node15", + "node16", + "node17", + "node18", + "node19" ], - "showSaveButton": false, - "showSubmitButton": true, - "choiceType": "checkbox" - } - ] - }, - { - "id": "node7", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node8" + "transitionLogic": { + "transitions": [ + { + "to": "group2" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null }, - "title": "Challenge Question Step", - "components": [ - { - "id": "ms0ene7xrh", - "type": "MultipleChoice", - "prompt": "

This is a multiple choice step that the student must answer correctly before they can move forward in the project. If the student gets the question wrong, they must go back to Step 1.1 in order to attempt the step again. This type of step is used to get students to review content when they get the question wrong.

\n

Bulbasaur is what type of Pokemon?

", - "choices": [ - { - "id": "0wxjeaix4n", - "text": "Grass/Poison", - "feedback": "Correct", - "isCorrect": true - }, - { - "id": "bwl0aqj90i", - "text": "Fire", - "feedback": "Sorry, Bulbasaur is not a fire type Pokemon.", - "isCorrect": false - }, - { - "id": "9yyn6ng60p", - "text": "Water", - "feedback": "Sorry, Bulbasaur is not a water type Pokemon.", - "isCorrect": false - } - ], + { + "id": "node1", + "type": "node", "showSaveButton": false, - "showSubmitButton": true, - "choiceType": "radio" - } - ] - }, - { - "id": "node8", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node9" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node2" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "HTML Step", + "components": [ + { + "id": "zh4h1urdys", + "type": "HTML", + "html": "\n\nhtml\n\n\n

This is a step where authors can enter their own html. Authors can also embed images and videos.

\n

\"\"

\n

\n\n" + } + ] }, - "title": "Match Sequence Step", - "components": [ - { - "id": "ja98ry0n0e", - "type": "Match", - "prompt": "

This is a step where students must drag and drop choices into the appropriate bucket.

\n

Match the controllers with the appropriate console. Then click the 'Submit Answer' button at the bottom of the page to check your answer.

", - "choices": [ - { - "id": "w2jmubyq0i", - "value": "", - "type": "choice" - }, - { - "id": "zxi4ncxv0x", - "value": "", - "type": "choice" - } - ], - "buckets": [ - { - "id": "rrisawrk19", - "value": "Playstation 4", - "type": "bucket" - }, - { - "id": "b04777i4m1", - "value": "Xbox One", - "type": "bucket" - } - ], - "feedback": [ - { - "bucketId": "rrisawrk19", - "choices": [ - { - "choiceId": "w2jmubyq0i", - "feedback": "Correct.", - "isCorrect": true, - "position": null, - "incorrectPositionFeedback": null - }, - { - "choiceId": "zxi4ncxv0x", - "feedback": "Incorrect", - "isCorrect": false, - "position": null, - "incorrectPositionFeedback": null - } - ] - }, - { - "bucketId": "b04777i4m1", - "choices": [ - { - "choiceId": "w2jmubyq0i", - "feedback": "Incorrect", - "isCorrect": false, - "position": null, - "incorrectPositionFeedback": null - }, - { - "choiceId": "zxi4ncxv0x", - "feedback": "Correct.", - "isCorrect": true, - "position": null, - "incorrectPositionFeedback": null - } - ] - } - ], - "showSaveButton": false, - "showSubmitButton": true, - "ordered": false - } - ] - }, - { - "id": "node9", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node10" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "id": "node2", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node3" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Open Response Step", + "components": [ + { + "id": "7edwu1p29b", + "type": "OpenResponse", + "prompt": "

This is a step where students enter text.

" + } + ] }, - "title": "Match Sequence Step Ordered", - "components": [ - { - "id": "8h4fxsg2wi", - "type": "Match", - "prompt": "

Authors can require the choices to be placed in a specific order within a bucket. Place the Playstation console images into the Playstation History bucket in the order of oldest to newest. Then click the 'Submit Answer' button at the bottom of the page to check your answer.

", - "choices": [ - { - "id": "87b5i860r9", - "value": "", - "type": "choice" - }, - { - "id": "zy2uvk0i2k", - "value": "", - "type": "choice" - }, - { - "id": "py0oik2bdx", - "value": "", - "type": "choice" - }, - { - "id": "cxi69s3hqo", - "value": "", - "type": "choice" - }, - { - "id": "kg0daqbcg4", - "value": "Xbone", - "type": "choice" - }, - { - "id": "1gfvbimpj3", - "value": "Xbox 360", - "type": "choice" - }, - { - "id": "sxfctzt39y", - "value": "Xbox 1", - "type": "choice" - } - ], - "buckets": [ - { - "id": "pf7ym43t9m", - "value": "Playstation History", - "type": "bucket" - }, - { - "id": "btvrrgacyr", - "value": "Xbox History", - "type": "bucket" - } - ], - "feedback": [ - { - "bucketId": "pf7ym43t9m", - "choices": [ - { - "choiceId": "87b5i860r9", - "feedback": "Correct.", - "isCorrect": true, - "position": 1, - "incorrectPositionFeedback": null - }, - { - "choiceId": "zy2uvk0i2k", - "feedback": "Correct.", - "isCorrect": true, - "position": 2, - "incorrectPositionFeedback": null - }, - { - "choiceId": "py0oik2bdx", - "feedback": "Correct.", - "isCorrect": true, - "position": 3, - "incorrectPositionFeedback": null - }, - { - "choiceId": "cxi69s3hqo", - "feedback": "Correct.", - "isCorrect": true, - "position": 4, - "incorrectPositionFeedback": null - }, - { - "choiceId": "kg0daqbcg4", - "feedback": "Incorrect", - "isCorrect": false, - "position": null, - "incorrectPositionFeedback": null - }, - { - "choiceId": "1gfvbimpj3", - "feedback": "Incorrect", - "isCorrect": false, - "position": null, - "incorrectPositionFeedback": null - }, - { - "choiceId": "sxfctzt39y", - "feedback": "Incorrect", - "isCorrect": false, - "position": null, - "incorrectPositionFeedback": null - } - ] - }, - { - "bucketId": "btvrrgacyr", - "choices": [ - { - "choiceId": "87b5i860r9", - "feedback": "Incorrect", - "isCorrect": false, - "position": null, - "incorrectPositionFeedback": null - }, - { - "choiceId": "zy2uvk0i2k", - "feedback": "Incorrect", - "isCorrect": false, - "position": null, - "incorrectPositionFeedback": null - }, - { - "choiceId": "py0oik2bdx", - "feedback": "Incorrect", - "isCorrect": false, - "position": null, - "incorrectPositionFeedback": null - }, - { - "choiceId": "cxi69s3hqo", - "feedback": "Incorrect", - "isCorrect": false, - "position": null, - "incorrectPositionFeedback": null - }, - { - "choiceId": "kg0daqbcg4", - "feedback": "Correct.", - "isCorrect": true, - "position": 3, - "incorrectPositionFeedback": null - }, - { - "choiceId": "1gfvbimpj3", - "feedback": "Correct.", - "isCorrect": true, - "position": 2, - "incorrectPositionFeedback": null - }, - { - "choiceId": "sxfctzt39y", - "feedback": "Correct.", - "isCorrect": true, - "position": 1, - "incorrectPositionFeedback": null - } - ] - } - ], - "showSaveButton": false, - "showSubmitButton": true, - "ordered": true - } - ] - }, - { - "id": "node10", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node11" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "id": "node3", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node4" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Open Response Step Auto Graded", + "components": [ + { + "id": "0sef5ya2wj", + "type": "OpenResponse", + "prompt": "

This is an auto-graded step that will give the student an automated score and automated text feedback. (Note: the auto graded functionality only works in a run and does not work when simply previewing the project)

\n

Explain how the sun helps animals survive.

" + } + ] }, - "title": "Questionnaire Step", - "components": [ - { - "id": "m97kyu4d4v", - "type": "HTML", - "html": "

This is a step where students must answer a series of questions that are either text or mulitple choice questions. This step is set up so that the student must answer all the questions before they can move to any other step. Once the student submits their answer, they can no longer change their answer. These restrictions can be turned on or off for this step type.

" - }, - { - "id": "yhrumlct2t", - "prompt": "What is your favorite type of food?", - "type": "OpenResponse" - }, - { - "id": "2upmb3om1q", - "prompt": "What is your favorite type of chocolate?", - "type": "MultipleChoice", - "choiceType": "radio", - "choices": [ - { - "id": "cri2anfnxg", - "text": "White", - "feedback": "" - }, - { - "id": "fzroe37gyx", - "text": "Dark", - "feedback": "" - }, - { - "id": "di49m0lc72", - "text": "Milk", - "feedback": "" - } - ], - "correctChoice": "fzroe37gyx" - }, - { - "id": "srof14u22x", - "prompt": "What is your favorite movie?", - "type": "OpenResponse" - }, - { - "id": "cjv5kq5290", - "prompt": "Edit prompt here", - "type": "MultipleChoice", - "choiceType": "checkbox", - "choices": [ - { - "id": "y1cdkafo2u", - "text": [ - "Enter choice text here" + { + "id": "node4", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node5" + } ], - "feedback": "" - }, - { - "id": "lcnhnc2myc", - "text": [ - "Enter choice text here" + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Note Step", + "components": [ + { + "id": "9rfz8yh32s", + "type": "OpenResponse", + "prompt": "

This is similar to an open response step but the text entry area opens in a popup so that students can still see the content from the previous step.

" + } + ] + }, + { + "id": "node5", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node6" + } ], - "feedback": "" - } + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Multiple Choice Step Single Answer", + "components": [ + { + "id": "09hahe7wsm", + "type": "MultipleChoice", + "prompt": "

This is a multiple choice step where the student is allowed to choose one choice.

\n

Who lives in a pineapple under the sea?

", + "choices": [ + { + "id": "y6rvd7eziz", + "text": "Spongebob", + "feedback": "Great Job!", + "isCorrect": true + }, + { + "id": "ti5rd0es02", + "text": "Patrick", + "feedback": "Patrick doesn't live in a pineapple, he lives under a rock", + "isCorrect": false + }, + { + "id": "gb0cnkaiem", + "text": "Squidward", + "feedback": "Squidward doesn't live in a pineapple, he lives in an Easter island head", + "isCorrect": false + } + ], + "showSaveButton": false, + "showSubmitButton": true, + "choiceType": "radio" + } ] - } - ] - }, - { - "id": "node11", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node12" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null }, - "title": "Draw Step", - "components": [ - { - "id": "9qwqc5uslq", - "type": "Draw", - "prompt": "

This is a draw step where the student can draw a diagram.

", - "stamps": { - "Stamps": [] - } - } - ] - }, - { - "id": "node12", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node13" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "id": "node6", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node7" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Multiple Choice Step Multiple Answer", + "components": [ + { + "id": "ajsdwjugeb", + "type": "MultipleChoice", + "prompt": "

This is a multiple choice step where the student is allowed to choose multiple choices.

\n

Which of these are Ninja Turtles?

", + "choices": [ + { + "id": "ofjky47v57", + "text": "Leonardo", + "feedback": "Great Job you know your Ninja Turtles", + "isCorrect": true + }, + { + "id": "dvsbhlfemx", + "text": "Donatello", + "feedback": "Great Job you know your Ninja Turtles", + "isCorrect": true + }, + { + "id": "i8et2px6m3", + "text": "Michelangelo", + "feedback": "Great Job you know your Ninja Turtles", + "isCorrect": true + }, + { + "id": "60gxmv4c76", + "text": "Raphael", + "feedback": "Great Job you know your Ninja Turtles", + "isCorrect": true + }, + { + "id": "2oxfbn09lk", + "text": "Squirtle", + "feedback": "Squirtle is a Pokemon", + "isCorrect": false + } + ], + "showSaveButton": false, + "showSubmitButton": true, + "choiceType": "checkbox" + } + ] }, - "title": "Draw Step Auto Graded", - "components": [ - { - "id": "nvjkidstry", - "type": "Draw", - "prompt": "

This is an auto graded draw step. Click the 'Submit' button to have your drawing auto graded.

\n

 

\n

(1) Use stamps to create TWO methane molecules (CH4). 

\n

(2) Create EXACTLY the number of oxygen molecules (O2) needed to react with your TWO methane molecules.

\n

(3) Create a new frame.

\n

(4) REARRANGE the atoms in the 2nd frame to make carbon dioxide (CO2) and water (H2O) molecules. Show clearly which atoms belong to which molecules.

", - "stamps": { - "Stamps": [ - "carbon.png", - "oxygen.png", - "hydrogen.png" - ] - } - } - ] - }, - { - "id": "node13", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node14" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "id": "node7", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node8" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Challenge Question Step", + "components": [ + { + "id": "ms0ene7xrh", + "type": "MultipleChoice", + "prompt": "

This is a multiple choice step that the student must answer correctly before they can move forward in the project. If the student gets the question wrong, they must go back to Step 1.1 in order to attempt the step again. This type of step is used to get students to review content when they get the question wrong.

\n

Bulbasaur is what type of Pokemon?

", + "choices": [ + { + "id": "0wxjeaix4n", + "text": "Grass/Poison", + "feedback": "Correct", + "isCorrect": true + }, + { + "id": "bwl0aqj90i", + "text": "Fire", + "feedback": "Sorry, Bulbasaur is not a fire type Pokemon.", + "isCorrect": false + }, + { + "id": "9yyn6ng60p", + "text": "Water", + "feedback": "Sorry, Bulbasaur is not a water type Pokemon.", + "isCorrect": false + } + ], + "showSaveButton": false, + "showSubmitButton": true, + "choiceType": "radio" + } + ] }, - "title": "Brainstorm Step", - "components": [ - { - "id": "dnz279bdvc", - "type": "Discussion", - "prompt": "

This is a forum discussion type of step where students can post their response and then see their classmates' responses. (Note: the brainstorm step only works in a run and does not work when simply previewing the project)

", - "showSaveButton": false, - "showSubmitButton": true, - "gateClassmateResponses": false - } - ] - }, - { - "id": "node14", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node15" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "id": "node8", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node9" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Match Sequence Step", + "components": [ + { + "id": "ja98ry0n0e", + "type": "Match", + "prompt": "

This is a step where students must drag and drop choices into the appropriate bucket.

\n

Match the controllers with the appropriate console. Then click the 'Submit Answer' button at the bottom of the page to check your answer.

", + "choices": [ + { + "id": "w2jmubyq0i", + "value": "", + "type": "choice" + }, + { + "id": "zxi4ncxv0x", + "value": "", + "type": "choice" + } + ], + "buckets": [ + { + "id": "rrisawrk19", + "value": "Playstation 4", + "type": "bucket" + }, + { + "id": "b04777i4m1", + "value": "Xbox One", + "type": "bucket" + } + ], + "feedback": [ + { + "bucketId": "rrisawrk19", + "choices": [ + { + "choiceId": "w2jmubyq0i", + "feedback": "Correct.", + "isCorrect": true, + "position": null, + "incorrectPositionFeedback": null + }, + { + "choiceId": "zxi4ncxv0x", + "feedback": "Incorrect", + "isCorrect": false, + "position": null, + "incorrectPositionFeedback": null + } + ] + }, + { + "bucketId": "b04777i4m1", + "choices": [ + { + "choiceId": "w2jmubyq0i", + "feedback": "Incorrect", + "isCorrect": false, + "position": null, + "incorrectPositionFeedback": null + }, + { + "choiceId": "zxi4ncxv0x", + "feedback": "Correct.", + "isCorrect": true, + "position": null, + "incorrectPositionFeedback": null + } + ] + } + ], + "showSaveButton": false, + "showSubmitButton": true, + "ordered": false + } + ] }, - "title": "Table Step", - "components": [ - { - "id": "y45mn2k7yl", - "type": "Table", - "prompt": "This is a table step where students fill out a table. You can make certain cells editable and other cells uneditable so the student can't change them. You can also allow the student to add/delete columns as well as add/delete rows.", - "globalCellSize": 10, - "tableData": [ - [ - { - "text": "Object", - "editable": false, - "size": null - }, + { + "id": "node9", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node10" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Match Sequence Step Ordered", + "components": [ { - "text": "Count", - "editable": false, - "size": null + "id": "8h4fxsg2wi", + "type": "Match", + "prompt": "

Authors can require the choices to be placed in a specific order within a bucket. Place the Playstation console images into the Playstation History bucket in the order of oldest to newest. Then click the 'Submit Answer' button at the bottom of the page to check your answer.

", + "choices": [ + { + "id": "87b5i860r9", + "value": "", + "type": "choice" + }, + { + "id": "zy2uvk0i2k", + "value": "", + "type": "choice" + }, + { + "id": "py0oik2bdx", + "value": "", + "type": "choice" + }, + { + "id": "cxi69s3hqo", + "value": "", + "type": "choice" + }, + { + "id": "kg0daqbcg4", + "value": "Xbone", + "type": "choice" + }, + { + "id": "1gfvbimpj3", + "value": "Xbox 360", + "type": "choice" + }, + { + "id": "sxfctzt39y", + "value": "Xbox 1", + "type": "choice" + } + ], + "buckets": [ + { + "id": "pf7ym43t9m", + "value": "Playstation History", + "type": "bucket" + }, + { + "id": "btvrrgacyr", + "value": "Xbox History", + "type": "bucket" + } + ], + "feedback": [ + { + "bucketId": "pf7ym43t9m", + "choices": [ + { + "choiceId": "87b5i860r9", + "feedback": "Correct.", + "isCorrect": true, + "position": 1, + "incorrectPositionFeedback": null + }, + { + "choiceId": "zy2uvk0i2k", + "feedback": "Correct.", + "isCorrect": true, + "position": 2, + "incorrectPositionFeedback": null + }, + { + "choiceId": "py0oik2bdx", + "feedback": "Correct.", + "isCorrect": true, + "position": 3, + "incorrectPositionFeedback": null + }, + { + "choiceId": "cxi69s3hqo", + "feedback": "Correct.", + "isCorrect": true, + "position": 4, + "incorrectPositionFeedback": null + }, + { + "choiceId": "kg0daqbcg4", + "feedback": "Incorrect", + "isCorrect": false, + "position": null, + "incorrectPositionFeedback": null + }, + { + "choiceId": "1gfvbimpj3", + "feedback": "Incorrect", + "isCorrect": false, + "position": null, + "incorrectPositionFeedback": null + }, + { + "choiceId": "sxfctzt39y", + "feedback": "Incorrect", + "isCorrect": false, + "position": null, + "incorrectPositionFeedback": null + } + ] + }, + { + "bucketId": "btvrrgacyr", + "choices": [ + { + "choiceId": "87b5i860r9", + "feedback": "Incorrect", + "isCorrect": false, + "position": null, + "incorrectPositionFeedback": null + }, + { + "choiceId": "zy2uvk0i2k", + "feedback": "Incorrect", + "isCorrect": false, + "position": null, + "incorrectPositionFeedback": null + }, + { + "choiceId": "py0oik2bdx", + "feedback": "Incorrect", + "isCorrect": false, + "position": null, + "incorrectPositionFeedback": null + }, + { + "choiceId": "cxi69s3hqo", + "feedback": "Incorrect", + "isCorrect": false, + "position": null, + "incorrectPositionFeedback": null + }, + { + "choiceId": "kg0daqbcg4", + "feedback": "Correct.", + "isCorrect": true, + "position": 3, + "incorrectPositionFeedback": null + }, + { + "choiceId": "1gfvbimpj3", + "feedback": "Correct.", + "isCorrect": true, + "position": 2, + "incorrectPositionFeedback": null + }, + { + "choiceId": "sxfctzt39y", + "feedback": "Correct.", + "isCorrect": true, + "position": 1, + "incorrectPositionFeedback": null + } + ] + } + ], + "showSaveButton": false, + "showSubmitButton": true, + "ordered": true } - ], - [ + ] + }, + { + "id": "node10", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node11" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Questionnaire Step", + "components": [ { - "text": "Computers", - "editable": true, - "size": null + "id": "m97kyu4d4v", + "type": "HTML", + "html": "

This is a step where students must answer a series of questions that are either text or mulitple choice questions. This step is set up so that the student must answer all the questions before they can move to any other step. Once the student submits their answer, they can no longer change their answer. These restrictions can be turned on or off for this step type.

" }, { - "text": "", - "editable": true, - "size": null - } - ], - [ - { - "text": "Phones", - "editable": true, - "size": null + "id": "yhrumlct2t", + "prompt": "What is your favorite type of food?", + "type": "OpenResponse" }, { - "text": "", - "editable": true, - "size": null - } - ], - [ + "id": "2upmb3om1q", + "prompt": "What is your favorite type of chocolate?", + "type": "MultipleChoice", + "choiceType": "radio", + "choices": [ + { + "id": "cri2anfnxg", + "text": "White", + "feedback": "" + }, + { + "id": "fzroe37gyx", + "text": "Dark", + "feedback": "" + }, + { + "id": "di49m0lc72", + "text": "Milk", + "feedback": "" + } + ], + "correctChoice": "fzroe37gyx" + }, { - "text": "", - "editable": true, - "size": null + "id": "srof14u22x", + "prompt": "What is your favorite movie?", + "type": "OpenResponse" }, { - "text": "", - "editable": true, - "size": null + "id": "cjv5kq5290", + "prompt": "Edit prompt here", + "type": "MultipleChoice", + "choiceType": "checkbox", + "choices": [ + { + "id": "y1cdkafo2u", + "text": [ + "Enter choice text here" + ], + "feedback": "" + }, + { + "id": "lcnhnc2myc", + "text": [ + "Enter choice text here" + ], + "feedback": "" + } + ] } - ] ] - }, - { - "id": "d9ioo01pa1", - "type": "OpenResponse", - "prompt": "" - } - ] - }, - { - "id": "node15", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node16" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null }, - "title": "Table Step Line Graph", - "components": [ - { - "id": "wtw3q7peip", - "type": "Table", - "prompt": "The table step can be authored to allow the student to graph the table data. Here is an example of using the table data to create a line graph. I have authored the step to pre-populate the table data so all you need to do is click \"Make Graph\".", - "globalCellSize": 10, - "tableData": [ - [ - { - "text": "x", - "editable": false, - "size": null - }, + { + "id": "node11", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node12" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Draw Step", + "components": [ { - "text": "y", - "editable": false, - "size": null + "id": "9qwqc5uslq", + "type": "Draw", + "prompt": "

This is a draw step where the student can draw a diagram.

", + "stamps": { + "Stamps": [] + } } - ], - [ + ] + }, + { + "id": "node12", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node13" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Draw Step Auto Graded", + "components": [ { - "text": "0", - "editable": true, - "size": null - }, + "id": "nvjkidstry", + "type": "Draw", + "prompt": "

This is an auto graded draw step. Click the 'Submit' button to have your drawing auto graded.

\n

 

\n

(1) Use stamps to create TWO methane molecules (CH4). 

\n

(2) Create EXACTLY the number of oxygen molecules (O2) needed to react with your TWO methane molecules.

\n

(3) Create a new frame.

\n

(4) REARRANGE the atoms in the 2nd frame to make carbon dioxide (CO2) and water (H2O) molecules. Show clearly which atoms belong to which molecules.

", + "stamps": { + "Stamps": [ + "carbon.png", + "oxygen.png", + "hydrogen.png" + ] + } + } + ] + }, + { + "id": "node13", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node14" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Brainstorm Step", + "components": [ { - "text": "0", - "editable": true, - "size": null + "id": "dnz279bdvc", + "type": "Discussion", + "prompt": "

This is a forum discussion type of step where students can post their response and then see their classmates' responses. (Note: the brainstorm step only works in a run and does not work when simply previewing the project)

", + "showSaveButton": false, + "showSubmitButton": true, + "gateClassmateResponses": false } - ], - [ + ] + }, + { + "id": "node14", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node15" + }, + { + "to": "node16" + }, + { + "to": "node17" + } + ], + "howToChooseAmongAvailablePaths": "workgroupId", + "whenToChoosePath": "enterNode", + "canChangePath": false, + "maxPathsVisitable": 1 + }, + "title": "Table Step", + "components": [ { - "text": "1", - "editable": true, - "size": null + "id": "y45mn2k7yl", + "type": "Table", + "prompt": "This is a table step where students fill out a table. You can make certain cells editable and other cells uneditable so the student can't change them. You can also allow the student to add/delete columns as well as add/delete rows.", + "globalCellSize": 10, + "tableData": [ + [ + { + "text": "Object", + "editable": false, + "size": null + }, + { + "text": "Count", + "editable": false, + "size": null + } + ], + [ + { + "text": "Computers", + "editable": true, + "size": null + }, + { + "text": "", + "editable": true, + "size": null + } + ], + [ + { + "text": "Phones", + "editable": true, + "size": null + }, + { + "text": "", + "editable": true, + "size": null + } + ], + [ + { + "text": "", + "editable": true, + "size": null + }, + { + "text": "", + "editable": true, + "size": null + } + ] + ], + "showAddToNotebookButton": true }, { - "text": "1", - "editable": true, - "size": null + "id": "d9ioo01pa1", + "type": "OpenResponse", + "prompt": "", + "showAddToNotebookButton": true } - ], - [ + ] + }, + { + "id": "node15", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [ { - "text": "2", - "editable": true, - "size": null + "id": "node15Constraint1", + "action": "makeThisNodeNotVisible", + "targetId": "node15", + "removalConditional": "all", + "removalCriteria": [ + { + "name": "branchPathTaken", + "params": { + "fromNodeId": "node14", + "toNodeId": "node15" + } + } + ] }, { - "text": "2", - "editable": true, - "size": null + "id": "node15Constraint2", + "action": "makeThisNodeNotVisitable", + "targetId": "node15", + "removalConditional": "all", + "removalCriteria": [ + { + "name": "branchPathTaken", + "params": { + "fromNodeId": "node14", + "toNodeId": "node15" + } + } + ] } - ], - [ + ], + "transitionLogic": { + "transitions": [ + { + "to": "node18" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Table Step Line Graph", + "components": [ { - "text": "3", - "editable": true, - "size": null + "id": "wtw3q7peip", + "type": "Table", + "prompt": "The table step can be authored to allow the student to graph the table data. Here is an example of using the table data to create a line graph. I have authored the step to pre-populate the table data so all you need to do is click \"Make Graph\".", + "globalCellSize": 10, + "tableData": [ + [ + { + "text": "x", + "editable": false, + "size": null + }, + { + "text": "y", + "editable": false, + "size": null + } + ], + [ + { + "text": "0", + "editable": true, + "size": null + }, + { + "text": "0", + "editable": true, + "size": null + } + ], + [ + { + "text": "1", + "editable": true, + "size": null + }, + { + "text": "1", + "editable": true, + "size": null + } + ], + [ + { + "text": "2", + "editable": true, + "size": null + }, + { + "text": "2", + "editable": true, + "size": null + } + ], + [ + { + "text": "3", + "editable": true, + "size": null + }, + { + "text": "6", + "editable": true, + "size": null + } + ], + [ + { + "text": "7", + "editable": true, + "size": null + }, + { + "text": "4", + "editable": true, + "size": null + } + ], + [ + { + "text": "10", + "editable": true, + "size": null + }, + { + "text": "4", + "editable": true, + "size": null + } + ] + ] }, { - "text": "6", - "editable": true, - "size": null + "id": "r2t378mp23", + "type": "OpenResponse", + "prompt": "" } - ], - [ + ] + }, + { + "id": "node16", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [ { - "text": "7", - "editable": true, - "size": null + "id": "node16Constraint1", + "action": "makeThisNodeNotVisible", + "targetId": "node16", + "removalConditional": "all", + "removalCriteria": [ + { + "name": "branchPathTaken", + "params": { + "fromNodeId": "node14", + "toNodeId": "node16" + } + } + ] }, { - "text": "4", - "editable": true, - "size": null + "id": "node16Constraint2", + "action": "makeThisNodeNotVisitable", + "targetId": "node16", + "removalConditional": "all", + "removalCriteria": [ + { + "name": "branchPathTaken", + "params": { + "fromNodeId": "node14", + "toNodeId": "node16" + } + } + ] } - ], - [ + ], + "transitionLogic": { + "transitions": [ + { + "to": "node18" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Table Step Bar Graph", + "components": [ { - "text": "10", - "editable": true, - "size": null + "id": "xxcdra15fe", + "type": "Table", + "prompt": "Here is an example of a bar graph.", + "globalCellSize": 10, + "tableData": [ + [ + { + "text": "Color", + "editable": false, + "size": null + }, + { + "text": "Count", + "editable": false, + "size": null + } + ], + [ + { + "text": "Blue", + "editable": true, + "size": null + }, + { + "text": "4", + "editable": true, + "size": null + } + ], + [ + { + "text": "Red", + "editable": true, + "size": null + }, + { + "text": "2", + "editable": true, + "size": null + } + ], + [ + { + "text": "Green", + "editable": true, + "size": null + }, + { + "text": "3", + "editable": true, + "size": null + } + ] + ] }, { - "text": "4", - "editable": true, - "size": null + "id": "cxwpukq3n5", + "type": "OpenResponse", + "prompt": "" } - ] ] - }, - { - "id": "r2t378mp23", - "type": "OpenResponse", - "prompt": "" - } - ] - }, - { - "id": "node16", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node17" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null }, - "title": "Table Step Bar Graph", - "components": [ - { - "id": "xxcdra15fe", - "type": "Table", - "prompt": "Here is an example of a bar graph.", - "globalCellSize": 10, - "tableData": [ - [ - { - "text": "Color", - "editable": false, - "size": null - }, - { - "text": "Count", - "editable": false, - "size": null - } - ], - [ + { + "id": "node17", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [ { - "text": "Blue", - "editable": true, - "size": null + "id": "node17Constraint1", + "action": "makeThisNodeNotVisible", + "targetId": "node17", + "removalConditional": "all", + "removalCriteria": [ + { + "name": "branchPathTaken", + "params": { + "fromNodeId": "node14", + "toNodeId": "node17" + } + } + ] }, { - "text": "4", - "editable": true, - "size": null + "id": "node17Constraint2", + "action": "makeThisNodeNotVisitable", + "targetId": "node17", + "removalConditional": "all", + "removalCriteria": [ + { + "name": "branchPathTaken", + "params": { + "fromNodeId": "node14", + "toNodeId": "node17" + } + } + ] } - ], - [ + ], + "transitionLogic": { + "transitions": [ + { + "to": "node18" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Table Step Pie Graph", + "components": [ { - "text": "Red", - "editable": true, - "size": null + "id": "ejryyaaf8n", + "type": "Table", + "prompt": "Here is an example of a pie graph.", + "globalCellSize": 10, + "tableData": [ + [ + { + "text": "Topping", + "editable": false, + "size": null + }, + { + "text": "Slices", + "editable": false, + "size": null + } + ], + [ + { + "text": "Pepperoni", + "editable": true, + "size": null + }, + { + "text": "4", + "editable": true, + "size": null + } + ], + [ + { + "text": "Mushroom", + "editable": true, + "size": null + }, + { + "text": "4", + "editable": true, + "size": null + } + ], + [ + { + "text": "Bell Pepper", + "editable": true, + "size": null + }, + { + "text": "2", + "editable": true, + "size": null + } + ] + ] }, { - "text": "2", - "editable": true, - "size": null + "id": "f1qmd09sw8", + "type": "OpenResponse", + "prompt": "" } - ], - [ + ] + }, + { + "id": "node18", + "type": "node", + "showSaveButton": true, + "showSubmitButton": true, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node19" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Outside Url Step", + "components": [ { - "text": "Green", - "editable": true, - "size": null - }, + "id": "u48j0eqq2t", + "type": "OutsideURL", + "url": "http://www.berkeley.edu/" + } + ] + }, + { + "id": "node19", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node20" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Label Step", + "components": [ { - "text": "3", - "editable": true, - "size": null + "id": "x4twq80e2n", + "prompt": "Label the picture.", + "type": "Label", + "backgroundImage": "ballon cars and skateboarder.png", + "canCreateLabels": true, + "canDeleteLabels": true, + "width": 800, + "height": 600, + "labels": [ + { + "pointX": 25, + "pointY": 200, + "textX": 100, + "textY": -25, + "text": "more potential energy", + "color": "blue" + }, + { + "pointX": 25, + "pointY": 250, + "textX": 100, + "textY": -25, + "text": "more potential energy", + "color": "blue" + }, + { + "pointX": 25, + "pointY": 300, + "textX": 100, + "textY": -25, + "text": "more kinetic energy", + "color": "green" + }, + { + "pointX": 25, + "pointY": 350, + "textX": 100, + "textY": -25, + "text": "more kinetic energy", + "color": "green" + } + ] } - ] ] - }, - { - "id": "cxwpukq3n5", - "type": "OpenResponse", - "prompt": "" - } - ] - }, - { - "id": "node17", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node18" + }, + { + "id": "group2", + "type": "group", + "title": "Example Features", + "startId": "node20", + "ids": [ + "node20", + "node21", + "node22", + "node23", + "node24", + "node25", + "node26", + "node27", + "node28", + "node29", + "node30", + "node31", + "node32", + "node33", + "node34" + ], + "transitionLogic": { + "transitions": [ + { + "to": "group3" + } + ] } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null }, - "title": "Table Step Pie Graph", - "components": [ - { - "id": "ejryyaaf8n", - "type": "Table", - "prompt": "Here is an example of a pie graph.", - "globalCellSize": 10, - "tableData": [ - [ - { - "text": "Topping", - "editable": false, - "size": null - }, + { + "id": "node20", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node21" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Show Previous Work 1", + "components": [ { - "text": "Slices", - "editable": false, - "size": null + "id": "onwwrjuqu1", + "type": "OpenResponse", + "prompt": "

The work you enter for this step will be displayed at the top of the next step.

" } - ], - [ - { - "text": "Pepperoni", - "editable": true, - "size": null - }, + ] + }, + { + "id": "node21", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node22" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Show Previous Work 2", + "components": [ { - "text": "4", - "editable": true, - "size": null + "id": "8n06hm4kaq", + "type": "OpenResponse", + "prompt": "

You should see your work from the previous step at the top of this step. The previous step and this step do not need to be the same type of step.

" } - ], - [ + ] + }, + { + "id": "node22", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node23" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Import Work 1", + "components": [ { - "text": "Mushroom", - "editable": true, - "size": null - }, + "id": "we8ry95yqa", + "type": "OpenResponse", + "prompt": "

The work from this step will be imported into the next step.

" + } + ] + }, + { + "id": "node23", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node24" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Import Work 2", + "components": [ { - "text": "4", - "editable": true, - "size": null + "id": "d6sa31zcw8", + "type": "OpenResponse", + "prompt": "

Your work from the previous step should be entered into the text area below. This feature works best if the previous step and this step are the same type of step.

" } - ], - [ + ] + }, + { + "id": "node24", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node25" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Must Complete Before Advancing", + "components": [ { - "text": "Bell Pepper", - "editable": true, - "size": null - }, + "id": "5o5x8tujpg", + "type": "OpenResponse", + "prompt": "

Authors can create constraints to control student navigation. For these constraint examples, I will be using open response steps but the constraints can be used with other step types as well. This step has been authored so that the student must complete the step before they can move forward. They are allowed to move backwards though.

" + } + ] + }, + { + "id": "node25", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node26" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Must Complete Before Exiting", + "components": [ { - "text": "2", - "editable": true, - "size": null + "id": "ade7umyd5r", + "type": "OpenResponse", + "prompt": "

This step has been authored so that the student can't leave the step until they have completed it.

" } - ] ] - }, - { - "id": "f1qmd09sw8", - "type": "OpenResponse", - "prompt": "" - } - ] - }, - { - "id": "node18", - "type": "node", - "showSaveButton": true, - "showSubmitButton": true, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node19" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null }, - "title": "Outside Url Step", - "components": [ - { - "id": "u48j0eqq2t", - "type": "OutsideURL", - "url": "http://www.berkeley.edu/" - } - ] - }, - { - "id": "node19", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node20" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "id": "node26", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node27" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Must Complete Previous Step 1", + "components": [ + { + "id": "3dvneo5lv3", + "type": "OpenResponse", + "prompt": "

Do not enter any work for this step. Try to move to the next step \"Must Complete Previous Step 2\", you should not be able to. Try to move to the step after the next step \"Must Complete Previous Step 3\", you should be able to. This feature is used to make the student complete a specific step before they can work on another specific step. Now enter some work for this step and click save. You should then be able to move to the next step.

" + } + ] }, - "title": "Label Step", - "components": [ - { - "id": "x4twq80e2n", - "prompt": "Label the picture.", - "type": "Label", - "backgroundImage": "ballon cars and skateboarder.png", - "canCreateLabels": true, - "canDeleteLabels": true, - "width": 800, - "height": 600, - "labels": [ - { - "pointX": 25, - "pointY": 200, - "textX": 100, - "textY": -25, - "text": "more potential energy", - "color": "blue" - }, - { - "pointX": 25, - "pointY": 250, - "textX": 100, - "textY": -25, - "text": "more potential energy", - "color": "blue" - }, - { - "pointX": 25, - "pointY": 300, - "textX": 100, - "textY": -25, - "text": "more kinetic energy", - "color": "green" - }, - { - "pointX": 25, - "pointY": 350, - "textX": 100, - "textY": -25, - "text": "more kinetic energy", - "color": "green" - } + { + "id": "node27", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node28" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Must Complete Previous Step 2", + "components": [ + { + "id": "b7zhe3k6m3", + "type": "OpenResponse", + "prompt": "

Good job, you completed \"Must Complete Previous Step 1\".

" + } ] - } - ] - }, - { - "id": "group2", - "type": "group", - "title": "Example Features", - "startId": "node20", - "ids": [ - "node20", - "node21", - "node22", - "node23", - "node24", - "node25", - "node26", - "node27", - "node28", - "node29", - "node30", - "node31", - "node32", - "node33", - "node34" - ] - }, - { - "id": "node20", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node21" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null }, - "title": "Show Previous Work 1", - "components": [ - { - "id": "onwwrjuqu1", - "type": "OpenResponse", - "prompt": "

The work you enter for this step will be displayed at the top of the next step.

" - } - ] - }, - { - "id": "node21", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node22" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "id": "node28", + "type": "node", + "showSaveButton": false, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node29" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Must Complete Previous Step 3", + "components": [ + { + "id": "dsre8jvudw", + "type": "HTML", + "html": "\n\nhtml\n\n\n

Now go back to \"Must Complete Previous Step 1\".

\n

\"\"

\n\n" + } + ] }, - "title": "Show Previous Work 2", - "components": [ - { - "id": "8n06hm4kaq", - "type": "OpenResponse", - "prompt": "

You should see your work from the previous step at the top of this step. The previous step and this step do not need to be the same type of step.

" - } - ] - }, - { - "id": "node22", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node23" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "id": "node29", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node30" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Aggregate Step 1", + "components": [ + { + "id": "qo7zqlxoa1", + "type": "MultipleChoice", + "prompt": "

The answers for this step will be aggregated for the whole class and displayed in the next step. (Note: the aggregating of class data only works in a run and does not work when simply previewing the project). The answer for this step will also determine which branch path you are assigned to in the Branching Point step.

\n

What is your favorite type of chocolate?

", + "choices": [ + { + "id": "isqtxst8ah", + "text": "Dark", + "feedback": "Thanks for submitting your answer", + "isCorrect": false + }, + { + "id": "araznpws0b", + "text": "Milk", + "feedback": "Thanks for submitting your answer", + "isCorrect": false + }, + { + "id": "fot1t36pcm", + "text": "White", + "feedback": "Thanks for submitting your answer", + "isCorrect": false + } + ], + "showSaveButton": false, + "showSubmitButton": false, + "choiceType": "radio" + } + ] }, - "title": "Import Work 1", - "components": [ - { - "id": "we8ry95yqa", - "type": "OpenResponse", - "prompt": "

The work from this step will be imported into the next step.

" - } - ] - }, - { - "id": "node23", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node24" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "id": "node30", + "type": "node", + "showSaveButton": true, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node31" + }, + { + "to": "node32" + }, + { + "to": "node33" + } + ], + "howToChooseAmongAvailablePaths": "criteriaMapping", + "whenToChoosePath": "enterNode", + "canChangePath": false, + "maxPathsVisitable": 1 + }, + "title": "Aggregate Step 2", + "components": [ + { + "id": "9ppjsfsmzb", + "type": "OpenResponse", + "prompt": "

You should see a pie graph with the aggregate student data for the class above.

" + } + ] }, - "title": "Import Work 2", - "components": [ - { - "id": "d6sa31zcw8", - "type": "OpenResponse", - "prompt": "

Your work from the previous step should be entered into the text area below. This feature works best if the previous step and this step are the same type of step.

" - } - ] - }, - { - "id": "node24", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node25" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "id": "node31", + "type": "node", + "showSaveButton": false, + "showSubmitButton": false, + "constraints": [ + { + "id": "constraint0", + "action": "makeThisNodeNotVisible", + "targetId": "node31", + "removalCriteria": [ + { + "functionName": "branchPathTaken", + "fromNodeId": "node30", + "toNodeId": "node31" + } + ] + }, + { + "id": "constraint1", + "action": "makeThisNodeNotVisitable", + "targetId": "node31", + "removalCriteria": [ + { + "functionName": "branchPathTaken", + "fromNodeId": "node30", + "toNodeId": "node31" + } + ] + } + ], + "transitionLogic": { + "transitions": [ + { + "to": "node34" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "You chose dark chocolate", + "components": [ + { + "id": "usk4gr3mq1", + "type": "HTML", + "html": "\n\nhtml\n\n\n

You chose \"Dark\" in the multiple choice step. Enjoy your dark chocolate!

\n

\"\"

\n\n" + } + ] }, - "title": "Must Complete Before Advancing", - "components": [ - { - "id": "5o5x8tujpg", - "type": "OpenResponse", - "prompt": "

Authors can create constraints to control student navigation. For these constraint examples, I will be using open response steps but the constraints can be used with other step types as well. This step has been authored so that the student must complete the step before they can move forward. They are allowed to move backwards though.

" - } - ] - }, - { - "id": "node25", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node26" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "id": "node32", + "type": "node", + "showSaveButton": false, + "showSubmitButton": false, + "constraints": [ + { + "id": "constraint2", + "action": "makeThisNodeNotVisible", + "targetId": "node32", + "removalCriteria": [ + { + "functionName": "branchPathTaken", + "fromNodeId": "node30", + "toNodeId": "node32" + } + ] + }, + { + "id": "constraint3", + "action": "makeThisNodeNotVisitable", + "targetId": "node32", + "removalCriteria": [ + { + "functionName": "branchPathTaken", + "fromNodeId": "node30", + "toNodeId": "node32" + } + ] + } + ], + "transitionLogic": { + "transitions": [ + { + "to": "node34" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "You chose milk chocolate", + "components": [ + { + "id": "6b4xu5mk8y", + "type": "HTML", + "html": "\n\nhtml\n\n\n

You chose \"Milk\" in the multiple choice step. Enjoy your milk chocolate!

\n

\"\"

\n\n" + } + ] }, - "title": "Must Complete Before Exiting", - "components": [ - { - "id": "ade7umyd5r", - "type": "OpenResponse", - "prompt": "

This step has been authored so that the student can't leave the step until they have completed it.

" - } - ] - }, - { - "id": "node26", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node27" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "id": "node33", + "type": "node", + "showSaveButton": false, + "showSubmitButton": false, + "constraints": [ + { + "id": "constraint4", + "action": "makeThisNodeNotVisible", + "targetId": "node33", + "removalCriteria": [ + { + "functionName": "branchPathTaken", + "fromNodeId": "node30", + "toNodeId": "node33" + } + ] + }, + { + "id": "constraint5", + "action": "makeThisNodeNotVisitable", + "targetId": "node33", + "removalCriteria": [ + { + "functionName": "branchPathTaken", + "fromNodeId": "node30", + "toNodeId": "node33" + } + ] + } + ], + "transitionLogic": { + "transitions": [ + { + "to": "node34" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "You chose white chocolate", + "components": [ + { + "id": "ycqqha8xn2", + "type": "HTML", + "html": "\n\nhtml\n\n\n

You chose \"White\" in the multiple choice step. Enjoy your white chocolate!

\n

\"\"

\n\n" + } + ] }, - "title": "Must Complete Previous Step 1", - "components": [ - { - "id": "3dvneo5lv3", - "type": "OpenResponse", - "prompt": "

Do not enter any work for this step. Try to move to the next step \"Must Complete Previous Step 2\", you should not be able to. Try to move to the step after the next step \"Must Complete Previous Step 3\", you should be able to. This feature is used to make the student complete a specific step before they can work on another specific step. Now enter some work for this step and click save. You should then be able to move to the next step.

" - } - ] - }, - { - "id": "node27", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node28" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "id": "node34", + "type": "node", + "showSaveButton": false, + "showSubmitButton": false, + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node790" + } + ], + "howToChooseAmongAvailablePaths": null, + "whenToChoosePath": null, + "canChangePath": null, + "maxPathsVisitable": null + }, + "title": "Merge point", + "components": [ + { + "id": "xabcha8d4g", + "type": "HTML", + "html": "\n\nhtml\n\n\n

Let us Merge!

\n\n", + "showAddToNotebookButton": true + } + ] }, - "title": "Must Complete Previous Step 2", - "components": [ - { - "id": "b7zhe3k6m3", - "type": "OpenResponse", - "prompt": "

Good job, you completed \"Must Complete Previous Step 1\".

" - } - ] - }, - { - "id": "node28", - "type": "node", - "showSaveButton": false, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node29" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "id": "group3", + "type": "group", + "title": "Third Lesson", + "startId": "node790", + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "group4" + } + ] + }, + "ids": [ + "node790", + "node791" + ] }, - "title": "Must Complete Previous Step 3", - "components": [ - { - "id": "dsre8jvudw", - "type": "HTML", - "html": "\n\nhtml\n\n\n

Now go back to \"Must Complete Previous Step 1\".

\n

\"\"

\n\n" - } - ] - }, - { - "id": "node29", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node30" - } - ], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "id": "node790", + "title": "Third Lesson Step 1", + "type": "node", + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node791" + } + ] + }, + "showSaveButton": false, + "showSubmitButton": false, + "components": [] }, - "title": "Aggregate Step 1", - "components": [ - { - "id": "qo7zqlxoa1", - "type": "MultipleChoice", - "prompt": "

The answers for this step will be aggregated for the whole class and displayed in the next step. (Note: the aggregating of class data only works in a run and does not work when simply previewing the project). The answer for this step will also determine which branch path you are assigned to in the Branching Point step.

\n

What is your favorite type of chocolate?

", - "choices": [ - { - "id": "isqtxst8ah", - "text": "Dark", - "feedback": "Thanks for submitting your answer", - "isCorrect": false - }, - { - "id": "araznpws0b", - "text": "Milk", - "feedback": "Thanks for submitting your answer", - "isCorrect": false - }, - { - "id": "fot1t36pcm", - "text": "White", - "feedback": "Thanks for submitting your answer", - "isCorrect": false - } - ], + { + "id": "node791", + "title": "Third Lesson Step 2", + "type": "node", + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "node792" + } + ] + }, "showSaveButton": false, "showSubmitButton": false, - "choiceType": "radio" - } - ] - }, - { - "id": "node30", - "type": "node", - "showSaveButton": true, - "showSubmitButton": false, - "constraints": [], - "transitionLogic": { - "transitions": [ - { - "to": "node31" + "components": [] + }, + { + "components": [ + { + "showAddToNotebookButton": true, + "showSubmitButton": false, + "showSaveButton": false, + "html": "
What do you want students to explore?

1. Outline and provide a brief description of the topics students can choose from for the Jigsaw activity here. You can add images and links using the tools above.

2. Edit the each jigsaw multiple-choice option below.

3. Do not add or remove any of the pre-populated choices, as this will break the branching logic that is built into the lesson. You only need to edit the Prompts you want students to choose from. If you would like to change the number of topics in the Jigsaw, let the WISE staff know and we can help.

", + "id": "nhbnnln5mm", + "type": "HTML", + "prompt": "" + }, + { + "choiceType": "radio", + "showAddToNotebookButton": true, + "showSubmitButton": true, + "showSaveButton": false, + "id": "0w3e2kgerw", + "type": "MultipleChoice", + "choices": [ + { + "feedback": "", + "id": "4z4f8xiqsj", + "text": "Edit this to be branch 1 choice", + "isCorrect": false + }, + { + "feedback": "", + "id": "2824d0tdlr", + "text": "Edit this to be branch 2 choice", + "isCorrect": false + } + ], + "prompt": "Add your prompt here. Then, edit the student choices below. Each choice will open a new branch.", + "showFeedback": false, + "maxSubmitCount": 1 + } + ], + "transitionLogic": { + "whenToChoosePath": "studentDataChanged", + "maxPathsVisitable": 1, + "howToChooseAmongAvailablePaths": "random", + "canChangePath": false, + "transitions": [ + { + "criteria": [ + { + "name": "choiceChosen", + "params": { + "componentId": "0w3e2kgerw", + "choiceIds": [ + "4z4f8xiqsj" + ], + "nodeId": "node792" + } + } + ], + "to": "node793" + }, + { + "criteria": [ + { + "name": "choiceChosen", + "params": { + "componentId": "0w3e2kgerw", + "choiceIds": [ + "2824d0tdlr" + ], + "nodeId": "node792" + } + } + ], + "to": "node795" + } + ] }, - { - "to": "node32" + "showSubmitButton": false, + "showSaveButton": false, + "id": "node792", + "title": "Pick your topic", + "type": "node", + "icons": { + "default": { + "imgAlt": "KI elicit ideas", + "type": "img", + "imgSrc": "wise5/themes/default/nodeIcons/ki-color-elicit.svg" + } }, - { - "to": "node33" - } - ], - "howToChooseAmongAvailablePaths": "criteriaMapping", - "whenToChoosePath": "enterNode", - "canChangePath": false, - "maxPathsVisitable": 1 + "constraints": [] }, - "title": "Aggregate Step 2", - "components": [ - { - "id": "9ppjsfsmzb", - "type": "OpenResponse", - "prompt": "

You should see a pie graph with the aggregate student data for the class above.

" - } - ] - }, - { - "id": "node31", - "type": "node", - "showSaveButton": false, - "showSubmitButton": false, - "constraints": [ - { - "id": "constraint0", - "action": "makeThisNodeNotVisible", - "targetId": "node31", - "removalCriteria": [ - { - "functionName": "branchPathTaken", - "fromNodeId": "node30", - "toNodeId": "node31" - } - ] - }, - { - "id": "constraint1", - "action": "makeThisNodeNotVisitable", - "targetId": "node31", - "removalCriteria": [ - { - "functionName": "branchPathTaken", - "fromNodeId": "node30", - "toNodeId": "node31" - } + { + "components": [ + { + "showAddToNotebookButton": true, + "showSubmitButton": false, + "showSaveButton": false, + "html": "
Topic A
1. Edit this to introduce the content for Topic A. 
2. Choose a web-based resource for students to review in the step below about the topic. If you don't want to use a resource, delete the component. You can add a different type of component if you'd like by clicking the + above and choosing the type of you want to add. Then edit that one.
3. Edit the open response prompt at the end of this sequence to prompt student reflection on the ideas you presented.
4. Delete this text once you are done.
", + "id": "if7yrkajl3", + "type": "HTML", + "prompt": "" + }, + { + "showAddToNotebookButton": true, + "showSubmitButton": false, + "showSaveButton": false, + "id": "vznhagylsn", + "type": "OutsideURL", + "prompt": "", + "url": "", + "height": 600 + }, + { + "showAddToNotebookButton": true, + "starterSentence": null, + "showSubmitButton": false, + "showSaveButton": false, + "id": "4t2qx4mwi6", + "type": "OpenResponse", + "prompt": "[EDIT REFLECTION PROMPT HERE] What did you notice about...? What did you learn about...? Record at least two observations about...", + "isStudentAttachmentEnabled": false + } + ], + "transitionLogic": { + "transitions": [ + { + "to": "node794" + } + ] + }, + "showSubmitButton": false, + "showSaveButton": true, + "id": "node793", + "title": "Topic A", + "type": "node", + "icons": { + "default": { + "imgAlt": "KI discover ideas", + "type": "img", + "imgSrc": "wise5/themes/default/nodeIcons/ki-color-add.svg" + } + }, + "constraints": [ + { + "removalConditional": "all", + "targetId": "node793", + "action": "makeThisNodeNotVisible", + "id": "node793Constraint1", + "removalCriteria": [ + { + "name": "branchPathTaken", + "params": { + "toNodeId": "node793", + "fromNodeId": "node792" + } + } + ] + }, + { + "removalConditional": "all", + "targetId": "node793", + "action": "makeThisNodeNotVisitable", + "id": "node793Constraint2", + "removalCriteria": [ + { + "name": "branchPathTaken", + "params": { + "toNodeId": "node793", + "fromNodeId": "node792" + } + } + ] + } ] - } - ], - "transitionLogic": { - "transitions": [{ - "to": "node34" - }], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null }, - "title": "You chose dark chocolate", - "components": [ - { - "id": "usk4gr3mq1", - "type": "HTML", - "html": "\n\nhtml\n\n\n

You chose \"Dark\" in the multiple choice step. Enjoy your dark chocolate!

\n

\"\"

\n\n" - } - ] - }, - { - "id": "node32", - "type": "node", - "showSaveButton": false, - "showSubmitButton": false, - "constraints": [ - { - "id": "constraint2", - "action": "makeThisNodeNotVisible", - "targetId": "node32", - "removalCriteria": [ - { - "functionName": "branchPathTaken", - "fromNodeId": "node30", - "toNodeId": "node32" - } - ] - }, - { - "id": "constraint3", - "action": "makeThisNodeNotVisitable", - "targetId": "node32", - "removalCriteria": [ - { - "functionName": "branchPathTaken", - "fromNodeId": "node30", - "toNodeId": "node32" - } + { + "components": [ + { + "showAddToNotebookButton": true, + "showSubmitButton": false, + "showSaveButton": false, + "html": "
Discuss what you learned about your Selected Topic A

1. For this step in the Jigsaw, you can choose to have students who chose the same topic discuss what they learned in pairs or small groups in the classroom (offline) or in an online discussion. This will help them synthesize what they have learned before sharing with students who chose another topic.

2. If you would like to use the online discussion, edit the Discussion component below that asks students to post what they've learned and comment on their peer's ideas. Feel free to edit the 'Prompt' text to better suit your needs.

3. If you would like students to work offline, add instructions here to help facilitate that process. Then delete the Discussion component below.

4. Customize the prompt for the Open Response component at the bottom of this step to prompt reflection what they learned about their topic - before they meet with students who studied the other topic. 

5. Delete this text once you are done.

", + "id": "qh56u3lupz", + "type": "HTML", + "prompt": "" + }, + { + "showAddToNotebookButton": false, + "starterSentence": null, + "showSubmitButton": false, + "connectedComponents": [ + { + "componentId": "4t2qx4mwi6", + "type": "showWork", + "nodeId": "node793" + } + ], + "showSaveButton": false, + "id": "e6zkfriesm", + "type": "OpenResponse", + "prompt": "Here is what you learned in the previous step:", + "isStudentAttachmentEnabled": false + }, + { + "showAddToNotebookButton": true, + "gateClassmateResponses": true, + "showSubmitButton": false, + "showSaveButton": false, + "id": "t84sg6f05k", + "type": "Discussion", + "prompt": "[Customize instructions]

Share what you've learned with the other members of Team [Enter topic #1 here].

Read the ideas from your teammates and comment on at least one.

When you're finished, answer the question at the bottom of the page.

", + "isStudentAttachmentEnabled": true + }, + { + "showAddToNotebookButton": true, + "starterSentence": null, + "showSubmitButton": false, + "showSaveButton": false, + "id": "hylmcrchzv", + "type": "OpenResponse", + "prompt": "[Customize reflection prompt] After meeting with another team [OR] reviewing your teammates' ideas, how have your ideas about XXXX changed? Make sure you mention...", + "isStudentAttachmentEnabled": false + } + ], + "transitionLogic": { + "transitions": [ + { + "to": "node797" + } + ] + }, + "showSubmitButton": false, + "showSaveButton": true, + "id": "node794", + "title": "Discuss what you learned about Topic A", + "type": "node", + "icons": { + "default": { + "imgAlt": "KI discover ideas", + "type": "img", + "imgSrc": "wise5/themes/default/nodeIcons/ki-color-add.svg" + } + }, + "constraints": [ + { + "removalConditional": "all", + "targetId": "node794", + "action": "makeThisNodeNotVisible", + "id": "node794Constraint1", + "removalCriteria": [ + { + "name": "branchPathTaken", + "params": { + "toNodeId": "node793", + "fromNodeId": "node792" + } + } + ] + }, + { + "removalConditional": "all", + "targetId": "node794", + "action": "makeThisNodeNotVisitable", + "id": "node794Constraint2", + "removalCriteria": [ + { + "name": "branchPathTaken", + "params": { + "toNodeId": "node793", + "fromNodeId": "node792" + } + } + ] + } ] - } - ], - "transitionLogic": { - "transitions": [{ - "to": "node34" - }], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null }, - "title": "You chose milk chocolate", - "components": [ - { - "id": "6b4xu5mk8y", - "type": "HTML", - "html": "\n\nhtml\n\n\n

You chose \"Milk\" in the multiple choice step. Enjoy your milk chocolate!

\n

\"\"

\n\n" - } - ] - }, - { - "id": "node33", - "type": "node", - "showSaveButton": false, - "showSubmitButton": false, - "constraints": [ - { - "id": "constraint4", - "action": "makeThisNodeNotVisible", - "targetId": "node33", - "removalCriteria": [ - { - "functionName": "branchPathTaken", - "fromNodeId": "node30", - "toNodeId": "node33" - } + { + "components": [ + { + "showAddToNotebookButton": true, + "showSubmitButton": false, + "showSaveButton": false, + "html": "
Topic B
1. Edit this to introduce the content for Topic B. 
2. Choose a web-based resource for students to review in the step below about the topic. If you don't want to use a resource, delete the component. You can add a different type of component if you'd like by clicking the + above and choosing the type of you want to add. Then edit that one.
3. Edit the open response prompt at the end of this sequence to prompt student reflection on the ideas you presented.
4. Delete this text once you are done.
", + "id": "k208vvl7g6", + "type": "HTML", + "prompt": "" + }, + { + "showAddToNotebookButton": true, + "showSubmitButton": false, + "showSaveButton": false, + "id": "4gx3373z38", + "type": "OutsideURL", + "prompt": "", + "url": "", + "height": 600 + }, + { + "showAddToNotebookButton": true, + "starterSentence": null, + "showSubmitButton": false, + "showSaveButton": false, + "id": "kd43661ip3", + "type": "OpenResponse", + "prompt": "[EDIT REFLECTION PROMPT HERE] What did you notice about...? What did you learn about...? Record at least two observations about...", + "isStudentAttachmentEnabled": false + } + ], + "transitionLogic": { + "transitions": [ + { + "to": "node796" + } + ] + }, + "showSubmitButton": false, + "showSaveButton": true, + "id": "node795", + "title": "Topic B", + "type": "node", + "icons": { + "default": { + "imgAlt": "KI discover ideas", + "type": "img", + "imgSrc": "wise5/themes/default/nodeIcons/ki-color-add.svg" + } + }, + "constraints": [ + { + "removalConditional": "all", + "targetId": "node795", + "action": "makeThisNodeNotVisible", + "id": "node795Constraint1", + "removalCriteria": [ + { + "name": "branchPathTaken", + "params": { + "toNodeId": "node795", + "fromNodeId": "node792" + } + } + ] + }, + { + "removalConditional": "all", + "targetId": "node795", + "action": "makeThisNodeNotVisitable", + "id": "node795Constraint2", + "removalCriteria": [ + { + "name": "branchPathTaken", + "params": { + "toNodeId": "node795", + "fromNodeId": "node792" + } + } + ] + } ] - }, - { - "id": "constraint5", - "action": "makeThisNodeNotVisitable", - "targetId": "node33", - "removalCriteria": [ - { - "functionName": "branchPathTaken", - "fromNodeId": "node30", - "toNodeId": "node33" - } + }, + { + "components": [ + { + "showAddToNotebookButton": true, + "showSubmitButton": false, + "showSaveButton": false, + "html": "
Discuss what you learned about your Selected Topic B

1. For this step in the Jigsaw, you can choose to have students who chose the same topic discuss what they learned in pairs or small groups in the classroom (offline) or in an online discussion. This will help them synthesize what they have learned before sharing with students who chose another topic.

2. If you would like to use the online discussion, edit the Discussion component below that asks students to post what they've learned and comment on their peer's ideas. Feel free to edit the 'Prompt' text to better suit your needs.

3. If you would like students to work offline, add instructions here to help facilitate that process. Then delete the Discussion component below.

4. Customize the prompt for the Open Response component at the bottom of this step to prompt reflection what they learned about their topic - before they meet with students who studied the other topic. 

5. Delete this text once you are done.

", + "id": "euc2uab7br", + "type": "HTML", + "prompt": "" + }, + { + "showAddToNotebookButton": false, + "starterSentence": null, + "showSubmitButton": false, + "connectedComponents": [ + { + "componentId": "4t2qx4mwi6", + "type": "showWork", + "nodeId": "node793" + } + ], + "showSaveButton": false, + "id": "4ds7yjc84z", + "type": "OpenResponse", + "prompt": "Here is what you learned in the previous step:", + "isStudentAttachmentEnabled": false + }, + { + "showAddToNotebookButton": true, + "gateClassmateResponses": true, + "showSubmitButton": false, + "showSaveButton": false, + "id": "pf7msod64f", + "type": "Discussion", + "prompt": "[Customize instructions]

Share what you've learned with the other members of Team XXXX.

Read the ideas from your teammates and comment on at least one.

When you're finished, answer the question at the bottom of the page.

", + "isStudentAttachmentEnabled": true + }, + { + "showAddToNotebookButton": true, + "starterSentence": null, + "showSubmitButton": false, + "showSaveButton": false, + "id": "c10pptolm8", + "type": "OpenResponse", + "prompt": "[Customize reflection prompt] After meeting with another team [OR] reviewing your teammates' ideas, how have your ideas about XXXX changed? Make sure you mention...", + "isStudentAttachmentEnabled": false + } + ], + "transitionLogic": { + "transitions": [ + { + "to": "node797" + } + ] + }, + "showSubmitButton": false, + "showSaveButton": true, + "id": "node796", + "title": "Discuss what you learned about Topic B", + "type": "node", + "icons": { + "default": { + "imgAlt": "KI discover ideas", + "type": "img", + "imgSrc": "wise5/themes/default/nodeIcons/ki-color-add.svg" + } + }, + "constraints": [ + { + "removalConditional": "all", + "targetId": "node796", + "action": "makeThisNodeNotVisible", + "id": "node796Constraint1", + "removalCriteria": [ + { + "name": "branchPathTaken", + "params": { + "toNodeId": "node795", + "fromNodeId": "node792" + } + } + ] + }, + { + "removalConditional": "all", + "targetId": "node796", + "action": "makeThisNodeNotVisitable", + "id": "node796Constraint2", + "removalCriteria": [ + { + "name": "branchPathTaken", + "params": { + "toNodeId": "node795", + "fromNodeId": "node792" + } + } + ] + } ] - } - ], - "transitionLogic": { - "transitions": [ { - "to": "node34" - }], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null }, - "title": "You chose white chocolate", - "components": [ - { - "id": "ycqqha8xn2", - "type": "HTML", - "html": "\n\nhtml\n\n\n

You chose \"White\" in the multiple choice step. Enjoy your white chocolate!

\n

\"\"

\n\n" - } - ] - }, - { - "id": "node34", - "type": "node", - "showSaveButton": false, - "showSubmitButton": false, - "constraints": [ - - ], - "transitionLogic": { - "transitions": [], - "howToChooseAmongAvailablePaths": null, - "whenToChoosePath": null, - "canChangePath": null, - "maxPathsVisitable": null + { + "components": [ + { + "showAddToNotebookButton": true, + "showSubmitButton": false, + "showSaveButton": false, + "html": "
Jigsaw Time!


Make connections between Topic A and Topic B

Group students from Branch A with those in Branch B.


1. Customize the instructions above to help students form a group with someone who studied the other topic. You can have stations, for example, and tell students that 2 who studied Topic A and 2 who studied Topic B should go to each station.


2. Edit the text above to add more specific instructions about the sharing process.


3. Delete this help text when you're finished.


", + "id": "eqhhrsfynt", + "type": "HTML", + "prompt": "" + } + ], + "transitionLogic": { + "transitions": [ + { + "to": "node798" + } + ] + }, + "showSubmitButton": false, + "showSaveButton": false, + "id": "node797", + "title": "Share what you learned", + "type": "node", + "icons": { + "default": { + "imgAlt": "KI distinguish ideas", + "type": "img", + "imgSrc": "wise5/themes/default/nodeIcons/ki-color-distinguish.svg" + } + }, + "constraints": [] }, - "title": "Merge point", - "components": [ - { - "id": "xabcha8d4g", - "type": "HTML", - "html": "\n\nhtml\n\n\n

Let us Merge!

\n\n" - } - ] - } + { + "components": [ + { + "showAddToNotebookButton": true, + "starterSentence": "I learned from the other team:\n1.\n2.", + "showSubmitButton": false, + "showSaveButton": false, + "id": "blvba48xfw", + "type": "OpenResponse", + "prompt": "[Customize this reflection prompt] You had a chance to talk with a [classmate/group] who had learned about a different [topic/question]. Record two things you learned from talking with a [classmate/group] on the other team.", + "isStudentAttachmentEnabled": false + }, + { + "showAddToNotebookButton": true, + "starterSentence": null, + "showSubmitButton": false, + "showSaveButton": false, + "id": "d84m1j8aao", + "type": "OpenResponse", + "prompt": "[Customize prompt] How does what you learned from the other team connect to the topic you investigated? Does it change or add to your understanding?", + "isStudentAttachmentEnabled": false + } + ], + "transitionLogic": { + "transitions": [] + }, + "showSubmitButton": false, + "showSaveButton": true, + "id": "node798", + "title": "What did you learn from your classmates?", + "type": "node", + "icons": { + "default": { + "imgAlt": "KI connect ideas", + "type": "img", + "imgSrc": "wise5/themes/default/nodeIcons/ki-color-connect.svg" + } + }, + "constraints": [] + }, + { + "startId": "node792", + "transitionLogic": { + "transitions": [] + }, + "ids": [ + "node792", + "node793", + "node794", + "node795", + "node796", + "node797", + "node798" + ], + "icons": { + "default": { + "color": "#2196F3", + "type": "font", + "fontSet": "material-icons", + "fontName": "info" + } + }, + "id": "group4", + "type": "group", + "title": "Jigsaw", + "constraints": [] + } ], "spaces": [ - { - "id": "sharePictures", - "name": "Public", - "isPublic": true, - "isShowInNotebook": false - } + { + "id": "sharePictures", + "name": "Public", + "isPublic": true, + "isShowInNotebook": false + } ], "constraints": [], "startGroupId": "group0", "startNodeId": "node1", "navigationMode": "guided", "layout": { - "template": "starmap|leftNav|rightNav", - "studentIsOnGroupNode": "layout3", - "studentIsOnApplicationNode": "layout4" + "template": "starmap|leftNav|rightNav", + "studentIsOnGroupNode": "layout3", + "studentIsOnApplicationNode": "layout4" }, "metadata": { - "title": "Demo Project" + "title": "Demo Project", + "authors": [ + { + "firstName": "g", + "lastName": "k", + "id": 3, + "username": "gk" + } + ] }, "inactiveNodes": [ - { - "id": "node789", - "title": "inactive node", - "type": "node", - "constraints": [], - "transitionLogic": { - "transitions": [] - }, - "showSaveButton": false, - "showSubmitButton": false, - "components": [], - "checked": false - } + { + "id": "node789", + "title": "inactive node", + "type": "node", + "constraints": [], + "transitionLogic": { + "transitions": [] + }, + "showSaveButton": false, + "showSubmitButton": false, + "components": [] + } ], "theme": "MyCustomTheme" -} +} \ No newline at end of file diff --git a/src/main/webapp/site/src/app/services/sessionService.spec.ts b/src/main/webapp/site/src/app/services/sessionService.spec.ts new file mode 100644 index 0000000000..305576a073 --- /dev/null +++ b/src/main/webapp/site/src/app/services/sessionService.spec.ts @@ -0,0 +1,143 @@ +import { TestBed, tick, fakeAsync } from '@angular/core/testing'; +import { SessionService } from '../../../../wise5/services/sessionService'; +import { UpgradeModule } from '@angular/upgrade/static'; +import ConfigService from '../../../../wise5/services/configService'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +let service: SessionService; +let configService: ConfigService; +let http: HttpTestingController; +const renewSessionURL = '/wise/session/renew'; + +describe('SessionService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ HttpClientTestingModule, UpgradeModule ], + providers: [ ConfigService, SessionService ] + }); + http = TestBed.get(HttpTestingController); + configService = TestBed.get(ConfigService); + service = TestBed.get(SessionService); + }); + + calculateIntervals(); + initializeSession(); + mouseMoved(); + checkMouseevent(); + checkForLogout(); + renewSession(); +}); + +function calculateIntervals() { + describe('calculateIntervals()', () => { + it('should calculate the warn and logout intervals when session timeout is 10 minutes', () => { + const sessionTimeout = 600; + const intervals = service.calculateIntervals(sessionTimeout); + expect(intervals.showWarningInterval).toEqual(540); + expect(intervals.forceLogoutAfterWarningInterval).toEqual(60); + }); + + it('should calculate the warn and logout intervals when session timeout is 30 minutes', () => { + const sessionTimeout = 1800; + const intervals = service.calculateIntervals(sessionTimeout); + expect(intervals.showWarningInterval).toEqual(1620); + expect(intervals.forceLogoutAfterWarningInterval).toEqual(180); + }); + + it('should calculate the warn and logout intervals when session timeout is 60 minutes', () => { + const sessionTimeout = 3600; + const intervals = service.calculateIntervals(sessionTimeout); + expect(intervals.showWarningInterval).toEqual(3300); + expect(intervals.forceLogoutAfterWarningInterval).toEqual(300); + }); + }); +} + +function initializeSession() { + describe('initializeSession()', () => { + it('should start check mouse event if not in preview mode', () => { + spyOn(configService, 'isPreview').and.returnValue(false); + const startCheckMouseEventSpy = spyOn(service, 'startCheckMouseEvent'); + service.initializeSession(); + expect(startCheckMouseEventSpy).toHaveBeenCalled(); + }); + }); +} + +function mouseMoved() { + describe('mouseMoved()', () => { + it('should set last activity timestamp when mouse is moved', () => { + service.mouseMoved(); + const renewSessionSpy = spyOn(service, 'renewSession'); + service.checkMouseEvent(); + expect(renewSessionSpy).toHaveBeenCalled(); + }); + }); +} + +function checkMouseevent() { + describe('checkMouseEvent()', () => { + it('should renew session if user has been active within last minute', () => { + spyOn(service, 'isActiveWithinLastMinute').and.returnValue(true); + const renewSessionSpy = spyOn(service, 'renewSession'); + service.checkMouseEvent(); + expect(renewSessionSpy).toHaveBeenCalled(); + }); + + it('should check for logout if user has not been active within last minute', () => { + spyOn(service, 'isActiveWithinLastMinute').and.returnValue(false); + const checkForLogoutSpy = spyOn(service, 'checkForLogout'); + service.checkMouseEvent(); + expect(checkForLogoutSpy).toHaveBeenCalled(); + }); + }); +} + +function checkForLogout() { + describe('checkForLogout()', () => { + it('should force logout when user is inactive for long enough', () => { + spyOn(service, 'isInactiveLongEnoughToForceLogout').and.returnValue(true); + const forceLogOutSpy = spyOn(service, 'forceLogOut'); + service.checkForLogout(); + expect(forceLogOutSpy).toHaveBeenCalled(); + }); + + it('should show warning when user is inactive for long enough to warn and warning is not showing', () => { + spyOn(service, 'isInactiveLongEnoughToForceLogout').and.returnValue(false); + spyOn(service, 'isInactiveLongEnoughToWarn').and.returnValue(true); + spyOn(service, 'isShowingWarning').and.returnValue(false); + const showWarningSpy = spyOn(service, 'showWarning'); + service.checkForLogout(); + expect(showWarningSpy).toHaveBeenCalled(); + }); + }); +} + +function renewSession() { + describe('renewSession()', () => { + it('should renew the session', fakeAsync( + () => { + spyOn(configService, 'getConfigParam') + .withArgs('renewSessionURL').and.returnValue(renewSessionURL); + const logOutSpy = spyOn(service, 'logOut') + service.renewSession(); + expect(configService.getConfigParam).toHaveBeenCalledWith('renewSessionURL'); + http.expectOne(renewSessionURL).flush('true'); + tick(); + expect(logOutSpy).not.toHaveBeenCalled(); + }) + ); + + it('should log the user out when renew session fails', fakeAsync( + () => { + spyOn(configService, 'getConfigParam') + .withArgs('renewSessionURL').and.returnValue(renewSessionURL); + const logOutSpy = spyOn(service, 'logOut'); + service.renewSession(); + expect(configService.getConfigParam).toHaveBeenCalledWith('renewSessionURL'); + http.expectOne(renewSessionURL).flush('false'); + tick(); + expect(logOutSpy).toHaveBeenCalled(); + }) + ); + }); +} \ No newline at end of file diff --git a/src/main/webapp/site/src/app/services/studentAssetService.spec.ts b/src/main/webapp/site/src/app/services/studentAssetService.spec.ts new file mode 100644 index 0000000000..275c5b9156 --- /dev/null +++ b/src/main/webapp/site/src/app/services/studentAssetService.spec.ts @@ -0,0 +1,80 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { UpgradeModule } from '@angular/upgrade/static'; +import { StudentAssetService } from '../../../../wise5/services/studentAssetService'; +import { ConfigService } from '../../../../wise5/services/configService'; + +let configService: ConfigService; +let service: StudentAssetService; +let http: HttpTestingController; +const studentAssetURL = '/student/asset/123'; +const workgroupId = 1; +const periodId = 256; +let asset1, asset2; + +describe('StudentAssetService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ HttpClientTestingModule, UpgradeModule ], + providers: [ StudentAssetService, ConfigService ] + }); + http = TestBed.get(HttpTestingController); + service = TestBed.get(StudentAssetService); + configService = TestBed.get(ConfigService); + }); + initialize(); + retrieveAssets(); + deleteAsset(); +}); + +function initialize() { + asset1 = {id:1, fileName:'wise.png', url:'/curriculum/1/wise.png'}; + asset2 = {id:2, fileName:'wiser.png', url:'/curriculum/1/wiser.png'}; +} + +function retrieveAssets() { + describe('retrieveAssets', () => { + beforeEach(() => { + spyOn(configService, 'getWorkgroupId').and.returnValue(workgroupId); + spyOn(configService, 'getConfigParam').withArgs('mode').and + .returnValue('studentRun').withArgs('studentAssetsURL').and + .returnValue(studentAssetURL); + }); + retrieveAssets_StudentMode_FetchAssetsAndSetAttributes(); + }); +} + +function deleteAsset() { + describe('deleteAsset', () => { + beforeEach(() => { + spyOn(configService, 'getWorkgroupId').and.returnValue(workgroupId); + spyOn(configService, 'getPeriodId').and.returnValue(periodId); + spyOn(configService, 'getConfigParam').withArgs('mode').and + .returnValue('studentRun').withArgs('studentAssetsURL').and + .returnValue(studentAssetURL); + }); + deleteAsset_StudentMode_DeleteAsset(); + }); +} + +function retrieveAssets_StudentMode_FetchAssetsAndSetAttributes() { + it('should fetch assets and set attributes', () => { + service.retrieveAssets().then((response) => { + expect(response.length).toEqual(1); + expect(response[0].type).toEqual('image'); + }); + http.expectOne(`${studentAssetURL}?workgroupId=${workgroupId}`).flush([asset1]); + }); +} + +function deleteAsset_StudentMode_DeleteAsset() { + it('should delete', () => { + service.allAssets = [asset2]; + expect(service.allAssets.length).toEqual(1); + service.deleteAsset(asset2); + const req = http.expectOne(`${studentAssetURL}/delete`); + expect(req.request.method).toEqual('POST'); + expect(req.request.body.studentAssetId).toEqual(2); + expect(req.request.body.clientDeleteTime).toBeDefined(); + }); +} diff --git a/src/main/webapp/site/src/app/student/student-angular-js-module.ts b/src/main/webapp/site/src/app/student/student-angular-js-module.ts index e5ed81c166..a9d0ff95ae 100644 --- a/src/main/webapp/site/src/app/student/student-angular-js-module.ts +++ b/src/main/webapp/site/src/app/student/student-angular-js-module.ts @@ -9,6 +9,9 @@ import { UtilService } from '../../../../wise5/services/utilService'; import { ConfigService } from '../../../../wise5/services/configService'; import { ProjectService } from '../../../../wise5/services/projectService'; import { VLEProjectService } from '../../../../wise5/vle/vleProjectService'; +import { CRaterService } from '../../../../wise5/services/cRaterService'; +import { SessionService } from '../../../../wise5/services/sessionService'; +import { StudentAssetService } from '../../../../wise5/services/studentAssetService'; @Component({template: ``}) export class EmptyComponent {} @@ -26,7 +29,10 @@ export class EmptyComponent {} providers: [ UtilService, ConfigService, + CRaterService, ProjectService, + SessionService, + StudentAssetService, VLEProjectService ] }) diff --git a/src/main/webapp/site/src/app/teacher/teacher-angular-js-module.ts b/src/main/webapp/site/src/app/teacher/teacher-angular-js-module.ts index b919b49385..6614d07a7c 100644 --- a/src/main/webapp/site/src/app/teacher/teacher-angular-js-module.ts +++ b/src/main/webapp/site/src/app/teacher/teacher-angular-js-module.ts @@ -11,6 +11,9 @@ import { ProjectService } from '../../../../wise5/services/projectService'; import { AuthoringToolProjectService } from '../../../../wise5/authoringTool/authoringToolProjectService'; import { ClassroomMonitorProjectService } from '../../../../wise5/classroomMonitor/classroomMonitorProjectService'; import { MilestoneReportDataComponent } from './milestone/milestone-report-data/milestone-report-data.component'; +import { CRaterService } from '../../../../wise5/services/cRaterService'; +import { SessionService } from '../../../../wise5/services/sessionService'; +import { StudentAssetService } from '../../../../wise5/services/studentAssetService'; @Component({template: ``}) export class EmptyComponent {} @@ -29,9 +32,12 @@ export class EmptyComponent {} providers: [ UtilService, ConfigService, + CRaterService, ProjectService, AuthoringToolProjectService, - ClassroomMonitorProjectService + ClassroomMonitorProjectService, + SessionService, + StudentAssetService ], entryComponents: [ MilestoneReportDataComponent diff --git a/src/main/webapp/site/src/assets/img/wise-students-hero-1200w.jpg b/src/main/webapp/site/src/assets/img/wise-students-hero-1200w.jpg new file mode 100644 index 0000000000..2cedda0f78 Binary files /dev/null and b/src/main/webapp/site/src/assets/img/wise-students-hero-1200w.jpg differ diff --git a/src/main/webapp/site/src/assets/img/wise-students-hero-1200w.webp b/src/main/webapp/site/src/assets/img/wise-students-hero-1200w.webp new file mode 100644 index 0000000000..a5a2d6aa32 Binary files /dev/null and b/src/main/webapp/site/src/assets/img/wise-students-hero-1200w.webp differ diff --git a/src/main/webapp/site/src/assets/img/wise-students-hero-600w.jpg b/src/main/webapp/site/src/assets/img/wise-students-hero-600w.jpg new file mode 100644 index 0000000000..5c3d877c93 Binary files /dev/null and b/src/main/webapp/site/src/assets/img/wise-students-hero-600w.jpg differ diff --git a/src/main/webapp/site/src/assets/img/wise-students-hero-600w.webp b/src/main/webapp/site/src/assets/img/wise-students-hero-600w.webp new file mode 100644 index 0000000000..833f29799c Binary files /dev/null and b/src/main/webapp/site/src/assets/img/wise-students-hero-600w.webp differ diff --git a/src/main/webapp/site/src/assets/img/wise-students-hero-900w.jpg b/src/main/webapp/site/src/assets/img/wise-students-hero-900w.jpg new file mode 100644 index 0000000000..b25ab601c3 Binary files /dev/null and b/src/main/webapp/site/src/assets/img/wise-students-hero-900w.jpg differ diff --git a/src/main/webapp/site/src/assets/img/wise-students-hero-900w.webp b/src/main/webapp/site/src/assets/img/wise-students-hero-900w.webp new file mode 100644 index 0000000000..992079addb Binary files /dev/null and b/src/main/webapp/site/src/assets/img/wise-students-hero-900w.webp differ diff --git a/src/main/webapp/site/src/tsconfig.app.json b/src/main/webapp/site/src/tsconfig.app.json index 07a5190538..bccd35bea8 100644 --- a/src/main/webapp/site/src/tsconfig.app.json +++ b/src/main/webapp/site/src/tsconfig.app.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "../out-tsc/app", "baseUrl": "./", - "types": ["jquery"], + "types": ["jquery", "node", "dom-mediacapture-record"], "paths": { "moment": ["../node_modules/moment/min/moment.min.js"] } diff --git a/src/main/webapp/site/src/tsconfig.spec.json b/src/main/webapp/site/src/tsconfig.spec.json index c771b489b4..3274c7f3b3 100644 --- a/src/main/webapp/site/src/tsconfig.spec.json +++ b/src/main/webapp/site/src/tsconfig.spec.json @@ -6,7 +6,8 @@ "types": [ "jasmine", "jquery", - "node" + "node", + "dom-mediacapture-record" ] }, "files": [ diff --git a/src/main/webapp/wise5/authoringTool/authoringTool.ts b/src/main/webapp/wise5/authoringTool/authoringTool.ts index be7787be55..80f21c6d09 100644 --- a/src/main/webapp/wise5/authoringTool/authoringTool.ts +++ b/src/main/webapp/wise5/authoringTool/authoringTool.ts @@ -24,7 +24,7 @@ import { AuthoringToolProjectService } from './authoringToolProjectService'; import AuthorNotebookController from './notebook/authorNotebookController'; import '../components/conceptMap/conceptMapAuthoringComponentModule'; import { ConfigService } from '../services/configService'; -import CRaterService from '../services/cRaterService'; +import { CRaterService } from '../services/cRaterService'; import '../directives/components'; import ComponentService from '../components/componentService'; import '../components/discussion/discussionAuthoringComponentModule'; @@ -56,16 +56,15 @@ import ProjectAssetController from './asset/projectAssetController'; import ProjectAssetService from '../services/projectAssetService'; import ProjectController from './project/projectController'; import ProjectInfoController from './info/projectInfoController'; -import PlanningService from '../services/planningService'; import RubricAuthoringController from './rubric/rubricAuthoringController'; -import SessionService from '../services/sessionService'; +import { SessionService } from '../services/sessionService'; import * as SockJS from 'sockjs-client'; import * as StompJS from '@stomp/stompjs'; window['SockJS'] = SockJS; window['Stomp'] = StompJS.Stomp; import SpaceService from '../services/spaceService'; import './structure/structureAuthoringModule'; -import StudentAssetService from '../services/studentAssetService'; +import { StudentAssetService } from '../services/studentAssetService'; import StudentDataService from '../services/studentDataService'; import StudentStatusService from '../services/studentStatusService'; import StudentWebSocketService from '../services/studentWebSocketService'; @@ -76,6 +75,7 @@ import TeacherWebSocketService from '../services/teacherWebSocketService'; import { UtilService } from '../services/utilService'; import WISELinkAuthoringController from './wiseLink/wiseLinkAuthoringController'; import * as moment from 'moment'; +import { AudioRecorderService } from '../services/audioRecorderService'; const authoringModule = angular .module('authoring', [ @@ -114,18 +114,18 @@ const authoringModule = angular 'ui.router' ]) .service('AnnotationService', AnnotationService) + .service('AudioRecorderService', AudioRecorderService) .service('ComponentService', ComponentService) .factory('ConfigService', downgradeInjectable(ConfigService)) - .service('CRaterService', CRaterService) + .factory('CRaterService', downgradeInjectable(CRaterService)) .service('NodeService', NodeService) .service('NotebookService', NotebookService) .service('NotificationService', NotificationService) - .service('PlanningService', PlanningService) .factory('ProjectService', downgradeInjectable(AuthoringToolProjectService)) .service('ProjectAssetService', ProjectAssetService) - .service('SessionService', SessionService) + .factory('SessionService', downgradeInjectable(SessionService)) .service('SpaceService', SpaceService) - .service('StudentAssetService', StudentAssetService) + .factory('StudentAssetService', downgradeInjectable(StudentAssetService)) .service('StudentDataService', StudentDataService) .service('StudentStatusService', StudentStatusService) .service('StudentWebSocketService', StudentWebSocketService) diff --git a/src/main/webapp/wise5/authoringTool/authoringToolController.ts b/src/main/webapp/wise5/authoringTool/authoringToolController.ts index 11129103af..fc2234a980 100644 --- a/src/main/webapp/wise5/authoringTool/authoringToolController.ts +++ b/src/main/webapp/wise5/authoringTool/authoringToolController.ts @@ -1,7 +1,7 @@ 'use strict'; import * as angular from 'angular'; import { ConfigService } from '../services/configService'; -import SessionService from '../services/sessionService'; +import { SessionService } from '../services/sessionService'; import TeacherDataService from '../services/teacherDataService'; import { AuthoringToolProjectService } from './authoringToolProjectService'; @@ -193,11 +193,15 @@ class AuthoringToolController { this.SessionService.closeWarningAndRenewSession(); }, () => { - this.SessionService.forceLogOut(); + this.logOut(); } ); }); + $scope.$on('logOut', () => { + this.logOut(); + }); + this.$scope.$on('showRequestLogout', ev => { const alert = this.$mdDialog .confirm() @@ -343,21 +347,31 @@ class AuthoringToolController { this.$rootScope.$broadcast('setGlobalMessage', { globalMessage: globalMessage }); } - saveEvent(eventName, category) { + logOut() { + this.saveEvent('logOut', 'Navigation').then(() => { + this.SessionService.logOut(); + }); + } + + saveEvent(eventName, category): any { const context = 'AuthoringTool'; const nodeId = null; const componentId = null; const componentType = null; const data = {}; - this.TeacherDataService.saveEvent( + const projectId = null; + return this.TeacherDataService.saveEvent( context, nodeId, componentId, componentType, category, eventName, - data - ); + data, + projectId + ).then((result) => { + return result; + }); } } diff --git a/src/main/webapp/wise5/authoringTool/components/shared/topBar/topBar.ts b/src/main/webapp/wise5/authoringTool/components/shared/topBar/topBar.ts index cd78f58d76..0a99b93be1 100644 --- a/src/main/webapp/wise5/authoringTool/components/shared/topBar/topBar.ts +++ b/src/main/webapp/wise5/authoringTool/components/shared/topBar/topBar.ts @@ -2,6 +2,8 @@ import { ConfigService } from '../../../../services/configService'; import { AuthoringToolProjectService } from '../../../authoringToolProjectService'; +import { SessionService } from '../../../../services/sessionService'; +import TeacherDataService from '../../../../services/teacherDataService'; class TopBarController { avatarColor: any; @@ -11,14 +13,23 @@ class TopBarController { contextPath: string; runId: number; - static $inject = ['$rootScope', '$state', '$window', 'ConfigService', 'ProjectService']; + static $inject = [ + '$rootScope', + '$state', + '$window', + 'ConfigService', + 'ProjectService', + 'SessionService' + ]; constructor( private $rootScope: any, private $state: any, private $window: any, private ConfigService: ConfigService, - private ProjectService: AuthoringToolProjectService + private ProjectService: AuthoringToolProjectService, + private SessionService: SessionService, + private TeacherDataService: TeacherDataService ) { this.workgroupId = this.ConfigService.getWorkgroupId(); if (this.workgroupId == null) { @@ -57,12 +68,31 @@ class TopBarController { goHome() { this.ProjectService.notifyAuthorProjectEnd().then(() => { - this.$rootScope.$broadcast('goHome'); + this.SessionService.goHome(); }); } logOut() { - this.$rootScope.$broadcast('logOut'); + const context = 'AuthoringTool'; + const category = 'Navigation'; + const eventName = 'logOutButtonClicked'; + const nodeId = null; + const componentId = null; + const componentType = null; + const data = {}; + const projectId = null; + this.TeacherDataService.saveEvent( + context, + nodeId, + componentId, + componentType, + category, + eventName, + data, + projectId + ).then((result) => { + this.SessionService.logOut(); + }); } } diff --git a/src/main/webapp/wise5/authoringTool/i18n/i18n_ar.json b/src/main/webapp/wise5/authoringTool/i18n/i18n_ar.json index dd3ce2e6f0..c9666b9360 100644 --- a/src/main/webapp/wise5/authoringTool/i18n/i18n_ar.json +++ b/src/main/webapp/wise5/authoringTool/i18n/i18n_ar.json @@ -156,7 +156,6 @@ "insertAsFirstStep": "أدرج كخطوة أولى", "insertInside": "إدراج بالداخل", "isCompleted": "اكتمل", - "isPlanningActivityCompleted": "اكتمل نشاط التخطيط", "isVisible": "مرئي", "isVisitable": "قابل للزيارة", "isVisited": "تمت زيارته", @@ -305,4 +304,4 @@ "youAreNotAllowedToInsertTheSelectedItemAfterItself": "غير مسموح لك بإدراج العنصر المُحدد بعده.", "youAreNotAllowedToInsertTheSelectedItemsAfterItself": "غير مسموح لك بإدراج العناصر المُحددة بعده.", "youCannotCopyActivitiesAtThisTime": "لا يُمكنك نسخ هذه الأنشطة في هذا الوقت." -} \ No newline at end of file +} diff --git a/src/main/webapp/wise5/authoringTool/i18n/i18n_el.json b/src/main/webapp/wise5/authoringTool/i18n/i18n_el.json index 8f685bead5..b9e8e41cd6 100644 --- a/src/main/webapp/wise5/authoringTool/i18n/i18n_el.json +++ b/src/main/webapp/wise5/authoringTool/i18n/i18n_el.json @@ -103,7 +103,6 @@ "insertAsFirstStep": "Εισαγωγή ως Πρώτο Βήμα", "insertInside": "Εισαγωγή Εντός", "isCompleted": "Είναι Ολοκληρωμένο", - "isPlanningActivityCompleted": "Η Δραστηριότητα Σχεδιασμού Είναι Ολοκληρωμένη", "isVisible": "Είναι Ορατό", "isVisitable": "Είναι Επισκέψιμο", "isVisited": "Έχει γίνει Επίσκεψη", @@ -193,4 +192,4 @@ "yes": "Ναι", "youAreNotAllowedToInsertTheSelectedItemAfterItself": "Δεν επιτρέπεται να εισάγετε το επιλεγμένο αντικείμενο ύστερα από το ίδιο", "youAreNotAllowedToInsertTheSelectedItemsAfterItself": "Δεν επιτρέπεται να εισάγετε τα επιλεγμένα αντικείμενα ύστερα από τα ίδια" -} \ No newline at end of file +} diff --git a/src/main/webapp/wise5/authoringTool/i18n/i18n_en.json b/src/main/webapp/wise5/authoringTool/i18n/i18n_en.json index 1597e6c18e..fea32bfcf4 100644 --- a/src/main/webapp/wise5/authoringTool/i18n/i18n_en.json +++ b/src/main/webapp/wise5/authoringTool/i18n/i18n_en.json @@ -206,7 +206,6 @@ "insertAsFirstStep": "Insert As First Step", "insertInside": "Insert Inside", "isCompleted": "Is Completed", - "isPlanningActivityCompleted": "Is Planning Lesson Completed", "isVisible": "Is Visible", "isVisitable": "Is Visitable", "isVisited": "Is Visited", diff --git a/src/main/webapp/wise5/authoringTool/i18n/i18n_zh_CN.json b/src/main/webapp/wise5/authoringTool/i18n/i18n_zh_CN.json index 526a0991cd..4ca6720e76 100644 --- a/src/main/webapp/wise5/authoringTool/i18n/i18n_zh_CN.json +++ b/src/main/webapp/wise5/authoringTool/i18n/i18n_zh_CN.json @@ -78,7 +78,6 @@ "insertAfter": "在后面插入", "insertInside": "在中间插入", "isCompleted": "已完成", - "isPlanningActivityCompleted": "规划活动完成了么", "isVisible": "是否可见", "isVisitable": "是否可以访问", "isVisited": "是否访问过", @@ -148,4 +147,4 @@ "viewHistory": "显示历史", "whenToChoosePath": "什么时候选择路径", "yes": "是" -} \ No newline at end of file +} diff --git a/src/main/webapp/wise5/authoringTool/i18n/i18n_zh_TW.json b/src/main/webapp/wise5/authoringTool/i18n/i18n_zh_TW.json index 78a3ec31a1..d1d3315edd 100755 --- a/src/main/webapp/wise5/authoringTool/i18n/i18n_zh_TW.json +++ b/src/main/webapp/wise5/authoringTool/i18n/i18n_zh_TW.json @@ -80,7 +80,6 @@ "insertAfter": "在後面插入", "insertInside": "在中間插入", "isCompleted": "已完成", - "isPlanningActivityCompleted": "規劃活動完成了嗎", "isVisible": "是否可見", "isVisitable": "是否可以訪問", "isVisited": "是否訪問過", @@ -156,4 +155,4 @@ "viewHistory": "顯示歷史", "whenToChoosePath": "什麼時候選擇路徑", "yes": "是" -} \ No newline at end of file +} diff --git a/src/main/webapp/wise5/authoringTool/main/authoringToolMainController.ts b/src/main/webapp/wise5/authoringTool/main/authoringToolMainController.ts index 0bba566c01..018dbd719e 100644 --- a/src/main/webapp/wise5/authoringTool/main/authoringToolMainController.ts +++ b/src/main/webapp/wise5/authoringTool/main/authoringToolMainController.ts @@ -64,9 +64,6 @@ class AuthoringToolMainController { if (this.is_rtl) { this.icons = { prev: 'arrow_forward', next: 'arrow_back' }; } - this.$rootScope.$on('logOut', () => { - this.saveEvent('logOut', 'Navigation', {}, null); - }); } getProjectByProjectId(projectId) { diff --git a/src/main/webapp/wise5/authoringTool/node/nodeAuthoringController.ts b/src/main/webapp/wise5/authoringTool/node/nodeAuthoringController.ts index d493bb07f0..e0499aa767 100644 --- a/src/main/webapp/wise5/authoringTool/node/nodeAuthoringController.ts +++ b/src/main/webapp/wise5/authoringTool/node/nodeAuthoringController.ts @@ -282,10 +282,6 @@ class NodeAuthoringController { } ] }, - { - value: 'isPlanningActivityCompleted', - text: this.$translate('isPlanningActivityCompleted') - }, { value: 'wroteXNumberOfWords', text: this.$translate('wroteXNumberOfWords'), diff --git a/src/main/webapp/wise5/authoringTool/structure/automatedAssessment/choose-item.html b/src/main/webapp/wise5/authoringTool/structure/automatedAssessment/choose-item.html index 065f36d28b..6028df20da 100644 --- a/src/main/webapp/wise5/authoringTool/structure/automatedAssessment/choose-item.html +++ b/src/main/webapp/wise5/authoringTool/structure/automatedAssessment/choose-item.html @@ -4,7 +4,7 @@
+ ng-class="::{'groupHeader': item.node.type == 'group', 'stepHeader': item.node.type != 'group'}">
{{item.node.title}}:
diff --git a/src/main/webapp/wise5/classroomMonitor/classroomMonitor.ts b/src/main/webapp/wise5/classroomMonitor/classroomMonitor.ts index 66a6e0d63e..f59190dca7 100644 --- a/src/main/webapp/wise5/classroomMonitor/classroomMonitor.ts +++ b/src/main/webapp/wise5/classroomMonitor/classroomMonitor.ts @@ -24,7 +24,7 @@ import ClassroomMonitorController from './classroomMonitorController'; import { ClassroomMonitorProjectService } from './classroomMonitorProjectService'; import '../components/conceptMap/conceptMapComponentModule'; import { ConfigService } from '../services/configService'; -import CRaterService from '../services/cRaterService'; +import { CRaterService } from '../services/cRaterService'; import '../directives/components'; import ComponentService from '../components/componentService'; import './dashboard/dashboardController'; @@ -58,13 +58,12 @@ import NotebookService from '../services/notebookService'; import NotificationService from '../services/notificationService'; import '../components/openResponse/openResponseComponentModule'; import '../components/outsideURL/outsideURLComponentModule'; -import PlanningService from '../services/planningService'; -import SessionService from '../services/sessionService'; +import { SessionService } from '../services/sessionService'; import * as SockJS from 'sockjs-client'; import * as StompJS from '@stomp/stompjs'; window['SockJS'] = SockJS; window['Stomp'] = StompJS.Stomp; -import StudentAssetService from '../services/studentAssetService'; +import { StudentAssetService } from '../services/studentAssetService'; import StudentDataService from '../services/studentDataService'; import StudentGradingController from './studentGrading/studentGradingController'; import StudentProgressController from './studentProgress/studentProgressController'; @@ -76,6 +75,7 @@ import TeacherDataService from '../services/teacherDataService'; import TeacherWebSocketService from '../services/teacherWebSocketService'; import { UtilService } from '../services/utilService'; import * as moment from 'moment'; +import { AudioRecorderService } from '../services/audioRecorderService'; const classroomMonitorModule = angular .module('classroomMonitor', [ @@ -115,18 +115,18 @@ const classroomMonitorModule = angular ]) .service('AchievementService', AchievementService) .service('AnnotationService', AnnotationService) + .service('AudioRecorderService', AudioRecorderService) .service('ComponentService', ComponentService) .factory('ConfigService', downgradeInjectable(ConfigService)) - .service('CRaterService', CRaterService) + .factory('CRaterService', downgradeInjectable(CRaterService)) .service('HttpInterceptor', HttpInterceptor) .service('MilestoneService', MilestoneService) .service('NodeService', NodeService) .service('NotebookService', NotebookService) .service('NotificationService', NotificationService) - .service('PlanningService', PlanningService) .factory('ProjectService', downgradeInjectable(ClassroomMonitorProjectService)) - .service('SessionService', SessionService) - .service('StudentAssetService', StudentAssetService) + .factory('SessionService', downgradeInjectable(SessionService)) + .factory('StudentAssetService', downgradeInjectable(StudentAssetService)) .service('StudentDataService', StudentDataService) .service('StudentStatusService', StudentStatusService) .service('StudentWebSocketService', StudentWebSocketService) diff --git a/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/navItem/navItem.html b/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/navItem/navItem.html index e7835255a5..c1d1bc880e 100644 --- a/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/navItem/navItem.html +++ b/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/navItem/navItem.html @@ -13,9 +13,6 @@ -
- {{ ::'noteThisActivityIncludesStudentPlanning' | translate }} -
- -
@@ -47,7 +35,6 @@ class="md-long-text noright list-item md-whiteframe-1dp" aria-label="{{::$ctrl.nodeTitle}}" ng-class="{'nav-item--list--group': $ctrl.isGroup, - 'nav-item--planning-mode': $ctrl.planningMode && $ctrl.isPlanningInstance, 'list-item--warn': $ctrl.newAlert, 'warn': $ctrl.newAlert, 'text-secondary': !$ctrl.nodeHasWork}"> diff --git a/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/navItem/navItem.ts b/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/navItem/navItem.ts index a8d2274397..52cced510a 100644 --- a/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/navItem/navItem.ts +++ b/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/navItem/navItem.ts @@ -3,7 +3,6 @@ import AnnotationService from '../../../../services/annotationService'; import { ConfigService } from '../../../../services/configService'; import NotificationService from '../../../../services/notificationService'; -import PlanningService from '../../../../services/planningService'; import { ClassroomMonitorProjectService } from '../../../classroomMonitorProjectService'; import StudentStatusService from '../../../../services/studentStatusService'; import TeacherDataService from '../../../../services/teacherDataService'; @@ -12,7 +11,6 @@ import * as $ from 'jquery'; class NavItemController { $translate: any; - availablePlanningNodes: any; alertIconClass: string; alertIconLabel: string; alertIconName: string; @@ -27,9 +25,6 @@ class NavItemController { icon: any; isCurrentNode: boolean; isGroup: boolean; - isParentGroupPlanning: boolean; - isPlanning: boolean; - isPlanningNode: boolean; isWorkgroupOnlineOnNode: boolean; item: any; maxScore: number; @@ -53,7 +48,6 @@ class NavItemController { 'AnnotationService', 'ConfigService', 'NotificationService', - 'PlanningService', 'ProjectService', 'StudentStatusService', 'TeacherDataService', @@ -68,7 +62,6 @@ class NavItemController { private AnnotationService: AnnotationService, private ConfigService: ConfigService, private NotificationService: NotificationService, - private PlanningService: PlanningService, private ProjectService: ClassroomMonitorProjectService, private StudentStatusService: StudentStatusService, private TeacherDataService: TeacherDataService, @@ -80,7 +73,6 @@ class NavItemController { this.AnnotationService = AnnotationService; this.ConfigService = ConfigService; this.NotificationService = NotificationService; - this.PlanningService = PlanningService; this.ProjectService = ProjectService; this.StudentStatusService = StudentStatusService; this.TeacherDataService = TeacherDataService; @@ -105,26 +97,12 @@ class NavItemController { this.maxScore = this.ProjectService.getMaxScoreForNode(this.nodeId); this.workgroupsOnNodeData = []; this.isWorkgroupOnlineOnNode = false; - this.isPlanning = this.PlanningService.isPlanning(this.nodeId); this.icon = this.ProjectService.getNodeIconByNodeId(this.nodeId); this.parentGroupId = null; var parentGroup = this.ProjectService.getParentGroup(this.nodeId); if (parentGroup != null) { this.parentGroupId = parentGroup.id; } - if (this.isPlanning) { - /* - * planning is enabled for this group so we will get the available - * planning nodes that can be used - */ - this.availablePlanningNodes = this.PlanningService.getAvailablePlanningNodes(this.nodeId); - } else if (this.isPlanningNode) { - /* this is an available planning node for its parent group, so we - * need to calculate the total number of times it has been added - * to the project by all the workgroups in the current period - */ - } - this.setWorkgroupsOnNodeData(); this.$onInit = () => { @@ -260,64 +238,6 @@ class NavItemController { } } - /** - * Returns the max times a planning node can be added to the project (-1 is - * is returned if there is no limit) - * @param planningNodeId - */ - getPlannindNodeMaxAllowed(planningNodeId) { - let maxAddAllowed = -1; // by default, students can add as many instances as they want - let planningGroupNode = null; - if (this.isParentGroupPlanning) { - planningGroupNode = this.ProjectService.getNodeById(this.parentGroupId); - } else { - planningGroupNode = this.ProjectService.getNodeById(this.nodeId); - } - // get the maxAddAllowed value by looking up the planningNode in the project. - if (planningGroupNode && planningGroupNode.availablePlanningNodes) { - for (let a = 0; a < planningGroupNode.availablePlanningNodes.length; a++) { - let availablePlanningNode = planningGroupNode.availablePlanningNodes[a]; - if (availablePlanningNode.nodeId === planningNodeId && availablePlanningNode.max != null) { - maxAddAllowed = availablePlanningNode.max; - } - } - } - - return maxAddAllowed; - } - - /** - * Returns the number of times a planning node has been added to the project - * @param planningNodeId - */ - getNumPlannindNodeInstances(planningNodeId) { - let numPlanningNodesAdded = 0; // keep track of number of instances - // otherwise, see how many times the planning node template has been used. - - let planningGroupNode = null; - if (this.isParentGroupPlanning) { - planningGroupNode = this.ProjectService.getNodeById(this.parentGroupId); - } else { - planningGroupNode = this.ProjectService.getNodeById(this.nodeId); - } - - // loop through the child ids in the planning group and see how many times they've been used - if (planningGroupNode && planningGroupNode.ids) { - for (let c = 0; c < planningGroupNode.ids.length; c++) { - let childPlanningNodeId = planningGroupNode.ids[c]; - let childPlanningNode = this.ProjectService.getNodeById(childPlanningNodeId); - if ( - childPlanningNode != null && - childPlanningNode.planningNodeTemplateId === planningNodeId - ) { - numPlanningNodesAdded++; - } - } - } - - return numPlanningNodesAdded; - } - /** * Get the node title * @param nodeId get the title for this node @@ -442,8 +362,7 @@ const NavItem = { bindings: { nodeId: '<', showPosition: '<', - type: '<', - isPlanningNode: '<' + type: '<' }, templateUrl: '/wise5/classroomMonitor/classroomMonitorComponents/nodeProgress/navItem/navItem.html', diff --git a/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/shared/topBar/topBar.ts b/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/shared/topBar/topBar.ts index 4b0dd37e21..2499cc23f2 100644 --- a/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/shared/topBar/topBar.ts +++ b/src/main/webapp/wise5/classroomMonitor/classroomMonitorComponents/shared/topBar/topBar.ts @@ -3,6 +3,7 @@ import { ConfigService } from '../../../../services/configService'; import { ClassroomMonitorProjectService } from '../../../classroomMonitorProjectService'; import TeacherDataService from '../../../../services/teacherDataService'; +import { SessionService } from '../../../../services/sessionService'; class TopBarController { $translate: any; @@ -22,7 +23,8 @@ class TopBarController { '$state', 'ConfigService', 'ProjectService', - 'TeacherDataService' + 'TeacherDataService', + 'SessionService' ]; constructor( @@ -31,7 +33,8 @@ class TopBarController { private $state: any, private ConfigService: ConfigService, private ProjectService: ClassroomMonitorProjectService, - private TeacherDataService: TeacherDataService + private TeacherDataService: TeacherDataService, + private SessionService: SessionService ) { this.$translate = $filter('translate'); this.workgroupId = this.ConfigService.getWorkgroupId(); @@ -125,29 +128,33 @@ class TopBarController { componentType, category, event, - eventData + eventData, + null ); - this.$rootScope.$broadcast('goHome'); + this.SessionService.goHome(); } logOut() { - var context = 'ClassroomMonitor'; - var nodeId = null; - var componentId = null; - var componentType = null; - var category = 'Navigation'; - var event = 'logOutButtonClicked'; - var eventData = {}; + const context = 'ClassroomMonitor'; + const category = 'Navigation'; + const eventName = 'logOutButtonClicked'; + const nodeId = null; + const componentId = null; + const componentType = null; + const data = {}; + const projectId = null; this.TeacherDataService.saveEvent( context, nodeId, componentId, componentType, category, - event, - eventData - ); - this.$rootScope.$broadcast('logOut'); + eventName, + data, + projectId + ).then((result) => { + this.SessionService.logOut(); + }); } } diff --git a/src/main/webapp/wise5/classroomMonitor/classroomMonitorController.ts b/src/main/webapp/wise5/classroomMonitor/classroomMonitorController.ts index 14edd99032..7e8197cae2 100644 --- a/src/main/webapp/wise5/classroomMonitor/classroomMonitorController.ts +++ b/src/main/webapp/wise5/classroomMonitor/classroomMonitorController.ts @@ -5,7 +5,7 @@ import NodeService from '../services/nodeService'; import NotebookService from '../services/notebookService'; import NotificationService from '../services/notificationService'; import TeacherDataService from '../services/teacherDataService'; -import SessionService from '../services/sessionService'; +import { SessionService } from '../services/sessionService'; import * as angular from 'angular'; import { ClassroomMonitorProjectService } from './classroomMonitorProjectService'; @@ -141,11 +141,15 @@ class ClassroomMonitorController { this.SessionService.closeWarningAndRenewSession(); }, () => { - this.SessionService.forceLogOut(); + this.logOut(); } ); }); + this.$scope.$on('logOut', () => { + this.logOut(); + }); + this.$scope.$on('showRequestLogout', ev => { const alert = $mdDialog .confirm() @@ -177,13 +181,14 @@ class ClassroomMonitorController { this.themePath = this.ProjectService.getThemePath(); this.notifications = this.NotificationService.notifications; - let context = 'ClassroomMonitor', + const context = 'ClassroomMonitor', nodeId = null, componentId = null, componentType = null, category = 'Navigation', event = 'sessionStarted', - data = {}; + data = {}, + projectId = null; this.TeacherDataService.saveEvent( context, nodeId, @@ -191,7 +196,8 @@ class ClassroomMonitorController { componentType, category, event, - data + data, + projectId ); this.$window.onbeforeunload = () => { @@ -251,6 +257,33 @@ class ClassroomMonitorController { this.$mdToast.hide(this.connectionLostDisplay); this.connectionLostShown = false; } + + logOut() { + this.saveEvent('logOut', 'Navigation').then(() => { + this.SessionService.logOut(); + }); + } + + saveEvent(eventName, category): any { + const context = 'ClassroomMonitor'; + const nodeId = null; + const componentId = null; + const componentType = null; + const data = {}; + const projectId = null; + return this.TeacherDataService.saveEvent( + context, + nodeId, + componentId, + componentType, + category, + eventName, + data, + projectId + ).then((result) => { + return result; + }); + } } export default ClassroomMonitorController; diff --git a/src/main/webapp/wise5/classroomMonitor/i18n/i18n_ar.json b/src/main/webapp/wise5/classroomMonitor/i18n/i18n_ar.json index 2bcba094ae..f8c3502ff9 100644 --- a/src/main/webapp/wise5/classroomMonitor/i18n/i18n_ar.json +++ b/src/main/webapp/wise5/classroomMonitor/i18n/i18n_ar.json @@ -105,7 +105,6 @@ "notAssigned": "لم يتم تعيينها", "notCompleted": "لم يتم الانتهاء منها", "notVisited": "لم تتم زيارته", - "noteThisActivityIncludesStudentPlanning": "ملاحظة: يتضمن هذا النشاط تخطيط الطالب", "numberOfAssessmentItems_0": "0 عناصر تقييم العمل ", "numberOfAssessmentItems_1": " 1 عنصر تقييم واحد ", "numberOfStudentOnStep": "#عدد من الطلاب بالخطوة ", @@ -184,4 +183,4 @@ "visited": "تمت الزيارة\n", "workgroups": "مجموعات العمل\n", "yes": "نعم" -} \ No newline at end of file +} diff --git a/src/main/webapp/wise5/classroomMonitor/i18n/i18n_el.json b/src/main/webapp/wise5/classroomMonitor/i18n/i18n_el.json index f85b6747f7..9f4759eeb5 100644 --- a/src/main/webapp/wise5/classroomMonitor/i18n/i18n_el.json +++ b/src/main/webapp/wise5/classroomMonitor/i18n/i18n_el.json @@ -89,7 +89,6 @@ "noMatchesFound": "Δεν βρέθηκαν αντιστοιχήσεις", "noNotes": "Η ομάδα δεν έχει ακόμα δημιουργήσει καθόλου σημειώσεις", "notCompleted": "Μη συμπληρωμένο", - "noteThisActivityIncludesStudentPlanning": "Σημείωση: Αυτή η δραστηριότητα περιλαμβάνει σχεδιασμό από τους μαθητές", "notVisited": "Δεν έχει γίνει επίσκεψη", "noWork": "Καμμία Δουλειά", "numberOfAssessmentItems_0": "0 αντικείμενα αξιολόγησης", @@ -166,4 +165,4 @@ "visited": "Έχει ήδη γίνει επίσκεψη", "workgroups": "Ομάδες Εργασίας", "yes": "Ναι" -} \ No newline at end of file +} diff --git a/src/main/webapp/wise5/classroomMonitor/i18n/i18n_en.json b/src/main/webapp/wise5/classroomMonitor/i18n/i18n_en.json index 97f202f67d..aa014e5fec 100644 --- a/src/main/webapp/wise5/classroomMonitor/i18n/i18n_en.json +++ b/src/main/webapp/wise5/classroomMonitor/i18n/i18n_en.json @@ -145,7 +145,6 @@ "notAssigned": "Not Assigned", "notCompleted": "Not Completed", "notes": "Notes", - "noteThisActivityIncludesStudentPlanning": "Note: This lesson includes student planning.", "notVisited": "Not Visited", "noWork": "No Work", "numberOfAssessmentItems_0": "0 assessment items", diff --git a/src/main/webapp/wise5/classroomMonitor/i18n/i18n_zh_CN.json b/src/main/webapp/wise5/classroomMonitor/i18n/i18n_zh_CN.json index 5922bc5e32..e846e524b7 100644 --- a/src/main/webapp/wise5/classroomMonitor/i18n/i18n_zh_CN.json +++ b/src/main/webapp/wise5/classroomMonitor/i18n/i18n_zh_CN.json @@ -42,7 +42,6 @@ "newWork": "新作品", "newWorkCounter": "新作品数: {{count}}", "noNotes": "学生还没有创建任何笔记", - "noteThisActivityIncludesStudentPlanning": "注意:该活动包含学生计划。", "notVisited": "未访问", "noWork": "无作品", "numberOfStudentOnStep": "# 在本步骤上的学生", @@ -85,4 +84,4 @@ "unpauseStudentScreens": "解锁学生屏幕", "visited": "已访问", "yes": "是" -} \ No newline at end of file +} diff --git a/src/main/webapp/wise5/classroomMonitor/i18n/i18n_zh_TW.json b/src/main/webapp/wise5/classroomMonitor/i18n/i18n_zh_TW.json index 49674ba128..6f7e1c66b3 100755 --- a/src/main/webapp/wise5/classroomMonitor/i18n/i18n_zh_TW.json +++ b/src/main/webapp/wise5/classroomMonitor/i18n/i18n_zh_TW.json @@ -49,7 +49,6 @@ "newWorkCounter": "新作業數量: {{count}}", "noMatchesFound": "找不到匹配的", "noNotes": "學生還沒有建立任何筆記", - "noteThisActivityIncludesStudentPlanning": "注意:此活動包含學生計劃。", "notVisited": "未訪問", "noWork": "無作業", "numberOfStudentOnStep": "# 在本步驟上的學生", @@ -100,4 +99,4 @@ "unpauseStudentScreens": "解鎖學生螢幕", "visited": "已訪問", "yes": "是" -} \ No newline at end of file +} diff --git a/src/main/webapp/wise5/components/componentController.ts b/src/main/webapp/wise5/components/componentController.ts index f9d0276316..ffb626ebf7 100644 --- a/src/main/webapp/wise5/components/componentController.ts +++ b/src/main/webapp/wise5/components/componentController.ts @@ -5,7 +5,7 @@ import { ConfigService } from "../services/configService"; import NodeService from "../services/nodeService"; import NotebookService from "../services/notebookService"; import { ProjectService } from "../services/projectService"; -import StudentAssetService from "../services/studentAssetService"; +import { StudentAssetService } from "../services/studentAssetService"; import { UtilService } from "../services/utilService"; import StudentDataService from "../services/studentDataService"; @@ -42,6 +42,7 @@ class ComponentController { isSubmit: boolean; saveMessage: any; isStudentAttachmentEnabled: boolean; + isStudentAudioRecordingEnabled: boolean; isPromptVisible: boolean; isSaveButtonVisible: boolean; isSubmitButtonVisible: boolean; @@ -114,7 +115,7 @@ class ComponentController { }; this.isStudentAttachmentEnabled = this.componentContent.isStudentAttachmentEnabled; - + this.isStudentAudioRecordingEnabled = this.componentContent.isStudentAudioRecordingEnabled || false; this.isPromptVisible = true; this.isSaveButtonVisible = false; this.isSubmitButtonVisible = false; @@ -1234,10 +1235,12 @@ class ComponentController { } attachStudentAsset(studentAsset) { - this.StudentAssetService.copyAssetForReference(studentAsset).then((copiedAsset) => { + return this.StudentAssetService.copyAssetForReference(studentAsset).then((copiedAsset) => { const attachment = { studentAssetId: copiedAsset.id, - iconURL: copiedAsset.iconURL + iconURL: copiedAsset.iconURL, + url: copiedAsset.url, + type: copiedAsset.type }; this.attachments.push(attachment); this.studentDataChanged(); @@ -1289,6 +1292,18 @@ class ComponentController { } }); } + + registerAudioRecordedListener() { + this.$scope.$on('audioRecorded', (event, {requester, audioFile}) => { + if (requester === `${this.nodeId}-${this.componentId}`) { + this.StudentAssetService.uploadAsset(audioFile).then((studentAsset) => { + this.attachStudentAsset(studentAsset).then(() => { + this.StudentAssetService.deleteAsset(studentAsset); + }) + }); + } + }); + } } export default ComponentController; diff --git a/src/main/webapp/wise5/components/conceptMap/conceptMapService.ts b/src/main/webapp/wise5/components/conceptMap/conceptMapService.ts index 3911d66dc7..a9aeaf34c4 100644 --- a/src/main/webapp/wise5/components/conceptMap/conceptMapService.ts +++ b/src/main/webapp/wise5/components/conceptMap/conceptMapService.ts @@ -1,7 +1,7 @@ import * as angular from 'angular'; import ComponentService from '../componentService'; import { ConfigService } from '../../services/configService'; -import StudentAssetService from '../../services/studentAssetService'; +import { StudentAssetService } from '../../services/studentAssetService'; import ConceptMapNode from './conceptMapNode'; import ConceptMapLink from './conceptMapLink'; diff --git a/src/main/webapp/wise5/components/draw/drawService.ts b/src/main/webapp/wise5/components/draw/drawService.ts index c423ef0307..b3cc89904a 100644 --- a/src/main/webapp/wise5/components/draw/drawService.ts +++ b/src/main/webapp/wise5/components/draw/drawService.ts @@ -2,7 +2,7 @@ import * as angular from 'angular'; import ComponentService from '../componentService'; -import StudentAssetService from '../../services/studentAssetService'; +import { StudentAssetService } from '../../services/studentAssetService'; class DrawService extends ComponentService { $q: any; diff --git a/src/main/webapp/wise5/components/embedded/embeddedService.ts b/src/main/webapp/wise5/components/embedded/embeddedService.ts index fffaf636b3..8ef7099bfd 100644 --- a/src/main/webapp/wise5/components/embedded/embeddedService.ts +++ b/src/main/webapp/wise5/components/embedded/embeddedService.ts @@ -1,7 +1,7 @@ import * as $ from 'jquery'; import * as html2canvas from 'html2canvas'; import ComponentService from '../componentService'; -import StudentAssetService from '../../services/studentAssetService'; +import { StudentAssetService } from '../../services/studentAssetService'; class EmbeddedService extends ComponentService { $q: any; diff --git a/src/main/webapp/wise5/components/label/labelService.ts b/src/main/webapp/wise5/components/label/labelService.ts index cb0398c3a2..b69ab0a659 100644 --- a/src/main/webapp/wise5/components/label/labelService.ts +++ b/src/main/webapp/wise5/components/label/labelService.ts @@ -1,6 +1,6 @@ import * as angular from 'angular'; import ComponentService from '../componentService'; -import StudentAssetService from '../../services/studentAssetService'; +import { StudentAssetService } from '../../services/studentAssetService'; class LabelService extends ComponentService { $q: any; diff --git a/src/main/webapp/wise5/components/match/matchController.ts b/src/main/webapp/wise5/components/match/matchController.ts index b5a634a8a0..f7f86373d1 100644 --- a/src/main/webapp/wise5/components/match/matchController.ts +++ b/src/main/webapp/wise5/components/match/matchController.ts @@ -22,6 +22,7 @@ class MatchController extends ComponentController { isLatestComponentStateSubmit: boolean; sourceBucket: any; privateNotebookItems: any[]; + autoScroll: any; static $inject = [ '$filter', @@ -79,6 +80,7 @@ class MatchController extends ComponentController { this.dragulaService = dragulaService; this.MatchService = MatchService; this.$mdMedia = $mdMedia; + this.autoScroll = require('dom-autoscroller'); this.choices = []; this.buckets = []; @@ -219,6 +221,15 @@ class MatchController extends ComponentController { this.disableDraggingIfNeeded(dragId); const drake = this.dragulaService.find(this.$scope, dragId).drake; this.showVisualIndicatorWhileDragging(drake); + this.autoScroll( + [document.querySelector('#content')], { + margin: 30, + pixels: 50, + scrollWhenOutside: true, + autoScroll: function() { + return this.down && drake.dragging; + } + }); } registerStudentDataChangedOnDrop(dragId) { diff --git a/src/main/webapp/wise5/components/openResponse/authoring.html b/src/main/webapp/wise5/components/openResponse/authoring.html index df6fdce5d2..f80ef985f1 100644 --- a/src/main/webapp/wise5/components/openResponse/authoring.html +++ b/src/main/webapp/wise5/components/openResponse/authoring.html @@ -25,6 +25,13 @@
{{ ::'advancedAuthoringOptions' | translate }}
ng-change='openResponseController.authoringViewComponentChanged()'/>
+
+ + {{ ::'openResponse.enableStudentAudioResponse' | translate }} + +
{{ ::'openResponse.enableCRater' | translate }}:
-
+
+ +
+
+ +
-
+
image {{ ::'openResponse.addFile' | translate }}
-
+
- cancel
+
+ + mic {{ ::'openResponse.recordResponse' | translate }} + + + stop {{ ::'openResponse.stopRecording' | translate }} + + + {{ ::'openResponse.recording' | translate }} {{ ::'openResponse.recordingTimeLeft' | translate }} :{{ openResponseController.getAudioRecordingTimeLeft() }} + +
+ + + + {{ ::'openResponse.deleteRecording' | translate }} + + cancel + +
+
{ - if (result != null) { - // get the CRater response - const data = result.data; + (data: any) => { + /* + * annotations we put in the component state will be + * removed from the component state and saved separately + */ + componentState.annotations = []; + + // get the CRater score + let score = data.score; + let concepts = data.concepts; + let previousScore = null; + if (data.scores != null) { + const maxSoFarFunc = (accumulator, currentValue) => { + return Math.max(accumulator, currentValue.score); + }; + score = data.scores.reduce(maxSoFarFunc, 0); + } - if (data != null) { - /* - * annotations we put in the component state will be - * removed from the component state and saved separately - */ - componentState.annotations = []; - - // get the CRater score - let score = data.score; - let concepts = data.concepts; - let previousScore = null; - if (data.scores != null) { - const maxSoFarFunc = (accumulator, currentValue) => { - return Math.max(accumulator, currentValue.score); - }; - score = data.scores.reduce(maxSoFarFunc, 0); - } + if (score != null) { + const autoScoreAnnotationData: any = { + value: score, + maxAutoScore: this.ProjectService.getMaxScoreForComponent( + this.nodeId, + this.componentId + ), + concepts: concepts, + autoGrader: 'cRater' + }; + if (data.scores != null) { + autoScoreAnnotationData.scores = data.scores; + } - if (score != null) { - const autoScoreAnnotationData: any = { - value: score, - maxAutoScore: this.ProjectService.getMaxScoreForComponent( - this.nodeId, - this.componentId - ), - concepts: concepts, - autoGrader: 'cRater' - }; - if (data.scores != null) { - autoScoreAnnotationData.scores = data.scores; - } + let autoScoreAnnotation = this.createAutoScoreAnnotation(autoScoreAnnotationData); - let autoScoreAnnotation = this.createAutoScoreAnnotation(autoScoreAnnotationData); + let annotationGroupForScore = null; - let annotationGroupForScore = null; + if (this.$scope.$parent.nodeController != null) { + // get the previous score and comment annotations + let latestAnnotations = this.$scope.$parent.nodeController.getLatestComponentAnnotations( + this.componentId + ); - if (this.$scope.$parent.nodeController != null) { - // get the previous score and comment annotations - let latestAnnotations = this.$scope.$parent.nodeController.getLatestComponentAnnotations( - this.componentId - ); + if ( + latestAnnotations != null && + latestAnnotations.score != null && + latestAnnotations.score.data != null + ) { + // get the previous score annotation value + previousScore = latestAnnotations.score.data.value; + } - if ( - latestAnnotations != null && - latestAnnotations.score != null && - latestAnnotations.score.data != null - ) { - // get the previous score annotation value - previousScore = latestAnnotations.score.data.value; - } + if ( + this.componentContent.enableGlobalAnnotations && + this.componentContent.globalAnnotationSettings != null + ) { + let globalAnnotationMaxCount = 0; + if ( + this.componentContent.globalAnnotationSettings.globalAnnotationMaxCount != + null + ) { + globalAnnotationMaxCount = this.componentContent.globalAnnotationSettings + .globalAnnotationMaxCount; + } + // get the annotation properties for the score that the student got. + annotationGroupForScore = this.ProjectService.getGlobalAnnotationGroupByScore( + this.componentContent, + previousScore, + score + ); + + // check if we need to apply this globalAnnotationSetting to this annotation: we don't need to if we've already reached the maxCount + if (annotationGroupForScore != null) { + let globalAnnotationGroupsByNodeIdAndComponentId = this.AnnotationService.getAllGlobalAnnotationGroups(); + annotationGroupForScore.annotationGroupCreatedTime = + autoScoreAnnotation.clientSaveTime; // save annotation creation time if ( - this.componentContent.enableGlobalAnnotations && - this.componentContent.globalAnnotationSettings != null + globalAnnotationGroupsByNodeIdAndComponentId.length >= + globalAnnotationMaxCount ) { - let globalAnnotationMaxCount = 0; - if ( - this.componentContent.globalAnnotationSettings.globalAnnotationMaxCount != - null - ) { - globalAnnotationMaxCount = this.componentContent.globalAnnotationSettings - .globalAnnotationMaxCount; - } - // get the annotation properties for the score that the student got. - annotationGroupForScore = this.ProjectService.getGlobalAnnotationGroupByScore( - this.componentContent, - previousScore, - score - ); - - // check if we need to apply this globalAnnotationSetting to this annotation: we don't need to if we've already reached the maxCount - if (annotationGroupForScore != null) { - let globalAnnotationGroupsByNodeIdAndComponentId = this.AnnotationService.getAllGlobalAnnotationGroups(); - annotationGroupForScore.annotationGroupCreatedTime = - autoScoreAnnotation.clientSaveTime; // save annotation creation time - - if ( - globalAnnotationGroupsByNodeIdAndComponentId.length >= - globalAnnotationMaxCount - ) { - // we've already applied this annotation properties to maxCount annotations, so we don't need to apply it any more. - annotationGroupForScore = null; - } - } - - if ( - annotationGroupForScore != null && - annotationGroupForScore.isGlobal && - annotationGroupForScore.unGlobalizeCriteria != null - ) { - // check if this annotation is global and what criteria needs to be met to un-globalize. - annotationGroupForScore.unGlobalizeCriteria.map(unGlobalizeCriteria => { - // if the un-globalize criteria is time-based (e.g. isVisitedAfter, isRevisedAfter, isVisitedAndRevisedAfter, etc), store the timestamp of this annotation in the criteria - // so we can compare it when we check for criteria satisfaction. - if (unGlobalizeCriteria.params != null) { - unGlobalizeCriteria.params.criteriaCreatedTimestamp = - autoScoreAnnotation.clientSaveTime; // save annotation creation time to criteria - } - }); - } - - if (annotationGroupForScore != null) { - // copy over the annotation properties into the autoScoreAnnotation's data - angular.merge(autoScoreAnnotation.data, annotationGroupForScore); - } + // we've already applied this annotation properties to maxCount annotations, so we don't need to apply it any more. + annotationGroupForScore = null; } } - componentState.annotations.push(autoScoreAnnotation); - - if (this.mode === 'authoring') { - if (this.latestAnnotations == null) { - this.latestAnnotations = {}; - } + if ( + annotationGroupForScore != null && + annotationGroupForScore.isGlobal && + annotationGroupForScore.unGlobalizeCriteria != null + ) { + // check if this annotation is global and what criteria needs to be met to un-globalize. + annotationGroupForScore.unGlobalizeCriteria.map(unGlobalizeCriteria => { + // if the un-globalize criteria is time-based (e.g. isVisitedAfter, isRevisedAfter, isVisitedAndRevisedAfter, etc), store the timestamp of this annotation in the criteria + // so we can compare it when we check for criteria satisfaction. + if (unGlobalizeCriteria.params != null) { + unGlobalizeCriteria.params.criteriaCreatedTimestamp = + autoScoreAnnotation.clientSaveTime; // save annotation creation time to criteria + } + }); + } - /* - * we are in the authoring view so we will set the - * latest score annotation manually - */ - this.latestAnnotations.score = autoScoreAnnotation; + if (annotationGroupForScore != null) { + // copy over the annotation properties into the autoScoreAnnotation's data + angular.merge(autoScoreAnnotation.data, annotationGroupForScore); } + } + } - let autoComment = null; + componentState.annotations.push(autoScoreAnnotation); - // get the submit counter - const submitCounter = this.submitCounter; + if (this.mode === 'authoring') { + if (this.latestAnnotations == null) { + this.latestAnnotations = {}; + } - if ( - this.componentContent.cRater.enableMultipleAttemptScoringRules && - submitCounter > 1 - ) { - /* - * this step has multiple attempt scoring rules and this is - * a subsequent submit - */ - // get the feedback based upon the previous score and current score - autoComment = this.CRaterService.getMultipleAttemptCRaterFeedbackTextByScore( - this.componentContent, - previousScore, - score - ); - } else { - // get the feedback text - autoComment = this.CRaterService.getCRaterFeedbackTextByScore( - this.componentContent, - score - ); - } + /* + * we are in the authoring view so we will set the + * latest score annotation manually + */ + this.latestAnnotations.score = autoScoreAnnotation; + } - if (autoComment != null) { - // create the auto comment annotation - const autoCommentAnnotationData: any = {}; - autoCommentAnnotationData.value = autoComment; - autoCommentAnnotationData.concepts = concepts; - autoCommentAnnotationData.autoGrader = 'cRater'; - - const autoCommentAnnotation = this.createAutoCommentAnnotation( - autoCommentAnnotationData - ); - - if (this.componentContent.enableGlobalAnnotations) { - if (annotationGroupForScore != null) { - // copy over the annotation properties into the autoCommentAnnotation's data - angular.merge(autoCommentAnnotation.data, annotationGroupForScore); - } - } - componentState.annotations.push(autoCommentAnnotation); + let autoComment = null; - if (this.mode === 'authoring') { - if (this.latestAnnotations == null) { - this.latestAnnotations = {}; - } + // get the submit counter + const submitCounter = this.submitCounter; - /* - * we are in the authoring view so we will set the - * latest comment annotation manually - */ - this.latestAnnotations.comment = autoCommentAnnotation; - } - } - if ( - this.componentContent.enableNotifications && - this.componentContent.notificationSettings && - this.componentContent.notificationSettings.notifications - ) { - const notificationForScore: any = this.ProjectService.getNotificationByScore( - this.componentContent, - previousScore, - score - ); - if (notificationForScore != null) { - notificationForScore.score = score; - notificationForScore.nodeId = this.nodeId; - notificationForScore.componentId = this.componentId; - this.NotificationService.sendNotificationForScore(notificationForScore); - } + if ( + this.componentContent.cRater.enableMultipleAttemptScoringRules && + submitCounter > 1 + ) { + /* + * this step has multiple attempt scoring rules and this is + * a subsequent submit + */ + // get the feedback based upon the previous score and current score + autoComment = this.CRaterService.getMultipleAttemptCRaterFeedbackTextByScore( + this.componentContent, + previousScore, + score + ); + } else { + // get the feedback text + autoComment = this.CRaterService.getCRaterFeedbackTextByScore( + this.componentContent, + score + ); + } + + if (autoComment != null) { + // create the auto comment annotation + const autoCommentAnnotationData: any = {}; + autoCommentAnnotationData.value = autoComment; + autoCommentAnnotationData.concepts = concepts; + autoCommentAnnotationData.autoGrader = 'cRater'; + + const autoCommentAnnotation = this.createAutoCommentAnnotation( + autoCommentAnnotationData + ); + + if (this.componentContent.enableGlobalAnnotations) { + if (annotationGroupForScore != null) { + // copy over the annotation properties into the autoCommentAnnotation's data + angular.merge(autoCommentAnnotation.data, annotationGroupForScore); } + } + componentState.annotations.push(autoCommentAnnotation); - // display global annotations dialog if needed - if ( - this.componentContent.enableGlobalAnnotations && - annotationGroupForScore != null && - annotationGroupForScore.isGlobal && - annotationGroupForScore.isPopup - ) { - this.$scope.$emit('displayGlobalAnnotations'); + if (this.mode === 'authoring') { + if (this.latestAnnotations == null) { + this.latestAnnotations = {}; } + + /* + * we are in the authoring view so we will set the + * latest comment annotation manually + */ + this.latestAnnotations.comment = autoCommentAnnotation; + } + } + if ( + this.componentContent.enableNotifications && + this.componentContent.notificationSettings && + this.componentContent.notificationSettings.notifications + ) { + const notificationForScore: any = this.ProjectService.getNotificationByScore( + this.componentContent, + previousScore, + score + ); + if (notificationForScore != null) { + notificationForScore.score = score; + notificationForScore.nodeId = this.nodeId; + notificationForScore.componentId = this.componentId; + this.NotificationService.sendNotificationForScore(notificationForScore); } } + + // display global annotations dialog if needed + if ( + this.componentContent.enableGlobalAnnotations && + annotationGroupForScore != null && + annotationGroupForScore.isGlobal && + annotationGroupForScore.isPopup + ) { + this.$scope.$emit('displayGlobalAnnotations'); + } } /* @@ -1035,6 +1036,63 @@ class OpenResponseController extends ComponentController { const action = 'change'; this.createComponentStateAndBroadcast(action); } + + startRecordingAudio() { + if (this.hasAudioResponses()) { + if (confirm(this.$translate(`openResponse.confirmReplaceAudioResponse`))) { + this.removeAudioAttachments(); + } else { + return; + } + } + this.AudioRecorderService.startRecording(`${this.nodeId}-${this.componentId}`); + this.startAudioCountdown(); + this.isRecordingAudio = true; + } + + startAudioCountdown() { + this.audioRecordingStartTime = new Date().getTime(); + this.audioRecordingInterval = setInterval(() => { + if (this.getAudioRecordingTimeLeft() <= 0) { + this.stopRecordingAudio(); + } + }, 500); + } + + stopRecordingAudio() { + this.AudioRecorderService.stopRecording(); + this.isRecordingAudio = false; + clearInterval(this.audioRecordingInterval); + } + + getAudioRecordingTimeElapsed() { + const now = new Date().getTime(); + return now - this.audioRecordingStartTime; + } + + getAudioRecordingTimeLeft() { + return Math.floor((this.audioRecordingMaxTime - this.getAudioRecordingTimeElapsed()) / 1000); + } + + hasAudioResponses() { + return this.attachments.filter(attachment => { + return attachment.type === 'audio'; + }).length > 0; + } + + removeAudioAttachment(attachment) { + if (confirm(this.$translate(`openResponse.confirmRemoveAudioResponse`))) { + this.removeAttachment(attachment); + } + } + + removeAudioAttachments() { + this.attachments.forEach(attachment => { + if (attachment.type === 'audio') { + this.removeAttachment(attachment); + } + }); + } } export default OpenResponseController; diff --git a/src/main/webapp/wise5/components/openResponse/openResponseService.ts b/src/main/webapp/wise5/components/openResponse/openResponseService.ts index 0a33b36467..361903aa6b 100644 --- a/src/main/webapp/wise5/components/openResponse/openResponseService.ts +++ b/src/main/webapp/wise5/components/openResponse/openResponseService.ts @@ -47,7 +47,7 @@ class OpenResponseService extends ComponentService { let studentData = componentState.studentData; if (studentData != null) { - if (studentData.response) { + if (studentData.response || studentData.attachments.length > 0) { // there is a response so the component is completed result = true; } @@ -111,7 +111,8 @@ class OpenResponseService extends ComponentService { hasResponse(componentState) { const response = componentState.studentData.response; - return response != null && response !== ''; + const attachments = componentState.studentData.attachments; + return (response != null && response !== '') || attachments.length > 0; } } diff --git a/src/main/webapp/wise5/components/table/tableService.ts b/src/main/webapp/wise5/components/table/tableService.ts index 93c9ad0b09..f92fcd8b55 100644 --- a/src/main/webapp/wise5/components/table/tableService.ts +++ b/src/main/webapp/wise5/components/table/tableService.ts @@ -1,7 +1,7 @@ import * as angular from 'angular'; import * as html2canvas from 'html2canvas'; import ComponentService from '../componentService'; -import StudentAssetService from '../../services/studentAssetService'; +import { StudentAssetService } from '../../services/studentAssetService'; class TableService extends ComponentService { $q: any; diff --git a/src/main/webapp/wise5/services/audioRecorderService.ts b/src/main/webapp/wise5/services/audioRecorderService.ts new file mode 100644 index 0000000000..282900ff1a --- /dev/null +++ b/src/main/webapp/wise5/services/audioRecorderService.ts @@ -0,0 +1,56 @@ +export class AudioRecorderService { + + mediaRecorder: MediaRecorder; + requester: string; + + static $inject = [ + '$rootScope', + ]; + + constructor(private $rootScope: any) { + } + + async init(constraints) { + try { + const stream = await navigator.mediaDevices.getUserMedia(constraints); + const options = { + mimeType: 'audio/webm' + }; + try { + this.mediaRecorder = new MediaRecorder(stream, options); + this.mediaRecorder.ondataavailable = (event: any) => { + this.$rootScope.$broadcast('audioRecorded', + { requester: this.requester, audioFile: this.getAudioFile(event.data) }); + } + this.mediaRecorder.start(); + } catch (e) { + console.error('Exception while creating MediaRecorder:', e); + return; + } + } catch (e) { + console.error('navigator.getUserMedia error:', e); + } + } + + getAudioFile(blob: Blob) { + const now = new Date().getTime(); + const filename = encodeURIComponent(`audio_${now}.webm`); + return new File([blob], filename, { + lastModified: now, + }); + } + + startRecording(requester: string) { + this.requester = requester; + const constraints = { + audio: { + echoCancellation: {exact: true} + } + }; + this.init(constraints); + } + + stopRecording() { + this.mediaRecorder.stop(); + } +} diff --git a/src/main/webapp/wise5/services/cRaterService.js b/src/main/webapp/wise5/services/cRaterService.ts similarity index 80% rename from src/main/webapp/wise5/services/cRaterService.js rename to src/main/webapp/wise5/services/cRaterService.ts index f00ebb7d7b..8ca189bbf0 100644 --- a/src/main/webapp/wise5/services/cRaterService.js +++ b/src/main/webapp/wise5/services/cRaterService.ts @@ -1,27 +1,35 @@ -class CRaterService { - constructor($http, ConfigService) { - this.$http = $http; - this.ConfigService = ConfigService; +'use strict'; + +import { Injectable } from "@angular/core"; +import { HttpClient, HttpParams } from "@angular/common/http"; +import { ConfigService } from "./configService"; + +@Injectable() +export class CRaterService { + constructor( + protected http: HttpClient, + protected ConfigService: ConfigService + ) { + } /** * Make a CRater request to score student data - * @param cRaterResponseId a randomly generated id used to keep track - * of the request + * @param cRaterItemId + * @param cRaterResponseId a randomly generated id used to keep track of the request * @param studentData the student data * @returns a promise that returns the result of the CRater request */ - makeCRaterScoringRequest(cRaterItemId, cRaterResponseId, studentData) { - const httpParams = { - method: 'GET', - url: this.ConfigService.getCRaterRequestURL() + '/score', - params: { - itemId: cRaterItemId, - responseId: cRaterResponseId, - studentData: studentData - } + makeCRaterScoringRequest(cRaterItemId: string, cRaterResponseId: number, studentData: any) { + const url = this.ConfigService.getCRaterRequestURL() + '/score'; + const params = new HttpParams() + .set('itemId', cRaterItemId) + .set('responseId', cRaterResponseId + '') + .set('studentData', studentData); + const options = { + params: params }; - return this.$http(httpParams).then((response) => { + return this.http.get(url, options).toPromise().then((response) => { return response; }); } @@ -30,7 +38,7 @@ class CRaterService { * Get the CRater item type from the component * @param component the component content */ - getCRaterItemType(component) { + getCRaterItemType(component: any) { if (component != null && component.cRater != null) { return component.cRater.itemType; } @@ -41,7 +49,7 @@ class CRaterService { * Get the CRater item id from the component * @param component the component content */ - getCRaterItemId(component) { + getCRaterItemId(component: any) { if (component != null && component.cRater != null) { return component.cRater.itemId; } @@ -53,7 +61,7 @@ class CRaterService { * @param component the component content * @returns when to perform the CRater scoring e.g. 'submit', 'save', 'change', 'exit' */ - getCRaterScoreOn(component) { + getCRaterScoreOn(component: any) { if (component != null) { /* * CRater can be enabled in two ways @@ -74,7 +82,7 @@ class CRaterService { * Check if CRater is enabled for this component * @param component the component content */ - isCRaterEnabled(component) { + isCRaterEnabled(component: any) { if (component != null) { // get the item type and item id const cRaterItemType = this.getCRaterItemType(component); @@ -91,7 +99,7 @@ class CRaterService { * @param component the component content * @returns whether the CRater is set to score on save */ - isCRaterScoreOnSave(component) { + isCRaterScoreOnSave(component: any) { if (component != null) { // find when we should perform the CRater scoring const scoreOn = this.getCRaterScoreOn(component); @@ -107,7 +115,7 @@ class CRaterService { * @param component the component content * @returns whether the CRater is set to score on submit */ - isCRaterScoreOnSubmit(component) { + isCRaterScoreOnSubmit(component: any) { if (component != null) { // find when we should perform the CRater scoring const scoreOn = this.getCRaterScoreOn(component); @@ -123,7 +131,7 @@ class CRaterService { * @param component the component content * @returns whether the CRater is set to score on change */ - isCRaterScoreOnChange(component) { + isCRaterScoreOnChange(component: any) { if (component != null) { // find when we should perform the CRater scoring const scoreOn = this.getCRaterScoreOn(component); @@ -139,7 +147,7 @@ class CRaterService { * @param component the component content * @returns whether the CRater is set to score on exit */ - isCRaterScoreOnExit(component) { + isCRaterScoreOnExit(component: any) { if (component != null) { // find when we should perform the CRater scoring const scoreOn = this.getCRaterScoreOn(component); @@ -156,7 +164,7 @@ class CRaterService { * @param score the score * @returns the scoring rule for the given score */ - getCRaterScoringRuleByScore(component, score) { + getCRaterScoringRuleByScore(component: any, score: any) { if (component != null && score != null) { const cRater = component.cRater; if (cRater != null) { @@ -186,7 +194,7 @@ class CRaterService { * @param score the score we want feedback for * @returns the feedback text for the given score */ - getCRaterFeedbackTextByScore(component, score) { + getCRaterFeedbackTextByScore(component: any, score: any) { const scoringRule = this.getCRaterScoringRuleByScore(component, score); if (scoringRule != null) { return scoringRule.feedbackText; @@ -201,8 +209,8 @@ class CRaterService { * @param currentScore the score from the current submit * @returns the feedback text for the given previous score and current score */ - getMultipleAttemptCRaterFeedbackTextByScore(component, previousScore, - currentScore) { + getMultipleAttemptCRaterFeedbackTextByScore(component: any, previousScore: any, + currentScore: any) { const scoringRule = this.getMultipleAttemptCRaterScoringRuleByScore( component, previousScore, currentScore); if (scoringRule != null) { @@ -219,8 +227,8 @@ class CRaterService { * @param currentScore the score from the current submit * @returns the scoring rule for the given previous score and current score */ - getMultipleAttemptCRaterScoringRuleByScore(component, previousScore, - currentScore) { + getMultipleAttemptCRaterScoringRuleByScore(component: any, previousScore: any, + currentScore: any) { if (component != null && previousScore != null && currentScore != null) { const cRater = component.cRater; if (cRater != null) { @@ -260,23 +268,14 @@ class CRaterService { * @param itemId A string. * @return A promise that returns whether the item id is valid. */ - makeCRaterVerifyRequest(itemId) { - const httpParams = { - method: 'GET', - url: this.ConfigService.getCRaterRequestURL() + '/verify', - params: { - itemId: itemId - } - }; - return this.$http(httpParams).then((response) => { - return response.data.isAvailable; + makeCRaterVerifyRequest(itemId: string) { + const url = this.ConfigService.getCRaterRequestURL() + '/verify'; + const params = new HttpParams().set('itemId', itemId); + const options = { + params: params + }; + return this.http.get(url, options).toPromise().then((response: any) => { + return response.isAvailable; }); } } - -CRaterService.$inject = [ - '$http', - 'ConfigService' -]; - -export default CRaterService; diff --git a/src/main/webapp/wise5/services/notificationService.js b/src/main/webapp/wise5/services/notificationService.js index 9ad1fccad5..a4b4540834 100644 --- a/src/main/webapp/wise5/services/notificationService.js +++ b/src/main/webapp/wise5/services/notificationService.js @@ -69,6 +69,10 @@ class NotificationService { retrieveNotifications() { + if (this.ConfigService.isPreview()) { + this.notifications = []; + return; + } const config = { method: 'GET', url: this.ConfigService.getNotificationURL(), diff --git a/src/main/webapp/wise5/services/planningService.js b/src/main/webapp/wise5/services/planningService.js deleted file mode 100644 index 966c0ce8cd..0000000000 --- a/src/main/webapp/wise5/services/planningService.js +++ /dev/null @@ -1,127 +0,0 @@ -'use strict'; - -class PlanningService { - constructor(ProjectService) { - this.ProjectService = ProjectService; - }; - - getPlanningNodes() { - return this.ProjectService.project.planningNodes; - }; - - /** - * Check if a node is a planning node - * @param nodeId the node id - * @returns whether the node is a planning node - */ - isPlanning(nodeId) { - const node = this.ProjectService.getNodeById(nodeId); - return node != null && node.planning; - } - - /** - * Check if a node is a planning node instance - * @param nodeId the node id - * @returns whether the node is a planning node instance - */ - isPlanningInstance(nodeId) { - const node = this.ProjectService.getNodeById(nodeId); - return node != null && node.planningNodeTemplateId; - } - - /** - * Get the available planning nodes for a given group - * @param nodeId the node id of the group - * @returns an array of planning node templates - */ - getAvailablePlanningNodes(nodeId) { - const availablePlanningNodesSoFar = []; - const node = this.ProjectService.getNodeById(nodeId); - if (node != null && node.availablePlanningNodes != null) { - const availablePlanningNodes = node.availablePlanningNodes; - for (let availablePlanningNode of availablePlanningNodes) { - const availablePlanningNodeActual = - this.ProjectService.getNodeById(availablePlanningNode.nodeId); - if (availablePlanningNodeActual != null) { - if (availablePlanningNode.max != null) { - availablePlanningNodeActual.max = availablePlanningNode.max; - } - availablePlanningNodesSoFar.push(availablePlanningNodeActual); - } - } - } - return availablePlanningNodesSoFar; - } - - /** - * Create a planning node instance and add it to the project - * @param nodeId the node id of the planning node template - * @param nextAvailablePlanningNodeId the node id of the planning node instance - */ - createPlanningNodeInstance(nodeId, nextAvailablePlanningNodeId) { - const planningNodeInstance = this.ProjectService.copyNode(nodeId); - planningNodeInstance.planningNodeTemplateId = nodeId; - planningNodeInstance.id = nextAvailablePlanningNodeId; - return planningNodeInstance; - } - - /** - * Add a planning node instance inside a group node - * @param nodeIdToInsertInside the group id to insert into - * @param planningNodeInstance the planning node instance to add - */ - addPlanningNodeInstanceInside(nodeIdToInsertInside, planningNodeInstance) { - const planningNodeInstanceNodeId = planningNodeInstance.id; - this.ProjectService.setIdToNode(planningNodeInstanceNodeId, planningNodeInstance); - this.ProjectService.addNode(planningNodeInstance); - this.ProjectService.insertNodeInsideOnlyUpdateTransitions(planningNodeInstanceNodeId, nodeIdToInsertInside); - this.ProjectService.insertNodeInsideInGroups(planningNodeInstanceNodeId, nodeIdToInsertInside); - this.ProjectService.recalculatePositionsInGroup(nodeIdToInsertInside); - this.ProjectService.calculateNodeOrderOfProject(); - } - - /** - * Add a planning node instance after a node - * @param nodeIdToInsertAfter the node to insert after - * @param planningNodeInstance the planning node instance to add - */ - addPlanningNodeInstanceAfter(nodeIdToInsertAfter, planningNodeInstance) { - const planningNodeInstanceNodeId = planningNodeInstance.id; - this.ProjectService.setIdToNode(planningNodeInstanceNodeId, planningNodeInstance); - this.ProjectService.addNode(planningNodeInstance); - this.ProjectService.insertNodeAfterInTransitions(planningNodeInstance, nodeIdToInsertAfter); - this.ProjectService.insertNodeAfterInGroups(planningNodeInstanceNodeId, nodeIdToInsertAfter); - const parentGroup = this.ProjectService.getParentGroup(nodeIdToInsertAfter); - this.ProjectService.recalculatePositionsInGroup(parentGroup.id); - this.ProjectService.calculateNodeOrderOfProject(); - } - - /** - * Move a planning node instance inside a group - * @param nodeIdToMove the node to move - * @param nodeIdToInsertInside the group to move the node into - */ - movePlanningNodeInstanceInside(nodeIdToMove, nodeIdToInsertInside) { - this.ProjectService.moveNodesInside([nodeIdToMove], nodeIdToInsertInside); - this.ProjectService.recalculatePositionsInGroup(nodeIdToInsertInside); - this.ProjectService.calculateNodeOrderOfProject(); - } - - /** - * Move a planning node instance after a node - * @param nodeIdToMove the node to move - * @param nodeIdToInsertAfter the other node to move the node after - */ - movePlanningNodeInstanceAfter(nodeIdToMove, nodeIdToInsertAfter) { - this.ProjectService.moveNodesAfter([nodeIdToMove], nodeIdToInsertAfter); - const parentGroup = this.ProjectService.getParentGroup(nodeIdToInsertAfter); - this.ProjectService.recalculatePositionsInGroup(parentGroup.id); - this.ProjectService.calculateNodeOrderOfProject(); - } -} - -PlanningService.$inject = [ - 'ProjectService' -]; - -export default PlanningService; diff --git a/src/main/webapp/wise5/services/projectService.ts b/src/main/webapp/wise5/services/projectService.ts index e24cc20d0e..3dcb42c00d 100644 --- a/src/main/webapp/wise5/services/projectService.ts +++ b/src/main/webapp/wise5/services/projectService.ts @@ -248,19 +248,11 @@ export class ProjectService { } } - loadPlanningNodes(planningNodes) { - for (const planningNode of planningNodes) { - this.setIdToNode(planningNode.id, planningNode); - // TODO: may need to add more function calls here to add the planning - } - } - parseProject() { this.clearProjectFields(); this.instantiateDefaults(); this.metadata = this.project.metadata; this.loadNodes(this.project.nodes); - this.loadPlanningNodes(this.project.planningNodes); this.loadInactiveNodes(this.project.inactiveNodes); this.loadConstraints(this.project.constraints); this.rootNode = this.getRootNode(this.project.nodes[0].id); @@ -275,7 +267,6 @@ export class ProjectService { instantiateDefaults() { this.project.nodes = this.project.nodes ? this.project.nodes : []; - this.project.planningNodes = this.project.planningNodes ? this.project.planningNodes : []; this.project.inactiveNodes = this.project.inactiveNodes ? this.project.inactiveNodes : []; this.project.constraints = this.project.constraints ? this.project.constraints : []; } @@ -1600,7 +1591,7 @@ export class ProjectService { const consumedNodes = []; for (const path of paths) { if (path.includes(nodeId)) { - const subPath = path.slice(0, path.indexOf(nodeId)); + const subPath = path.splice(0, path.indexOf(nodeId)); for (const nodeIdInPath of subPath) { if (!consumedNodes.includes(nodeIdInPath)) { consumedNodes.push(nodeIdInPath); @@ -2939,11 +2930,19 @@ export class ProjectService { * ] */ for (let transitionCopy of transitionsCopy) { - // insert a transition from the node we are removing - transitions.splice(insertIndex, 0, transitionCopy); - insertIndex++; + if (!this.isTransitionExist(transitions, transitionCopy)) { + const toNodeId = transitionCopy.to; + if (this.isApplicationNode(node.id) && this.isGroupNode(toNodeId) && + this.hasGroupStartId(toNodeId)) { + this.addToTransition(node, this.getGroupStartId(toNodeId)); + } else { + transitions.splice(insertIndex, 0, transitionCopy); + insertIndex++; + } + } } } + t--; // check if the node we are moving is a group if (this.isGroupNode(nodeId)) { @@ -2961,6 +2960,15 @@ export class ProjectService { } } + if (transitions.length === 0 && parentIdOfNodeToRemove != 'group0' && + parentIdOfNodeToRemove != this.getParentGroupId(node.id)) { + /* + * the from node no longer has any transitions so we will make it transition to the + * parent of the node we are removing + */ + this.addToTransition(node, parentIdOfNodeToRemove); + } + if (this.isBranchPoint(nodeId)) { /* * the node we are deleting is a branch point so we to @@ -2990,6 +2998,15 @@ export class ProjectService { } } + isTransitionExist(transitions: any[], transition: any) { + for (const tempTransition of transitions) { + if (tempTransition.from === transition.from && tempTransition.to === transition.to) { + return true; + } + } + return false; + } + /** * Remove the node id from all groups * @param nodeId the node id to remove @@ -3487,12 +3504,6 @@ export class ProjectService { fromNodeTitle: fromNodeTitle, toNodeTitle: toNodeTitle }); - } else if (name === 'isPlanningActivityCompleted') { - const nodeId = params.nodeId; - if (nodeId != null) { - const nodeTitle = this.getNodePositionAndTitleByNodeId(nodeId); - message += this.upgrade.$injector.get('$filter')('translate')('completeNodeTitle', { nodeTitle: nodeTitle }); - } } else if (name === 'wroteXNumberOfWords') { const nodeId = params.nodeId; if (nodeId != null) { @@ -3587,6 +3598,11 @@ export class ProjectService { return this.getNodeById(nodeId).startId; } + hasGroupStartId(nodeId) { + const startId = this.getGroupStartId(nodeId); + return startId != null && startId != ''; + } + /** * Update the transitions so that the fromGroup points to the newToGroup * diff --git a/src/main/webapp/wise5/services/sessionService.js b/src/main/webapp/wise5/services/sessionService.js deleted file mode 100644 index 4e4b92c30a..0000000000 --- a/src/main/webapp/wise5/services/sessionService.js +++ /dev/null @@ -1,144 +0,0 @@ -class SessionService { - constructor($http, $location, $rootScope, ConfigService) { - this.$http = $http; - this.$location = $location; - this.$rootScope = $rootScope; - this.ConfigService = ConfigService; - this.warningVisible = false; - this.defaultForceLogoutAfterWarningInterval = this.convertMinutesToSeconds(5); - const intervals = this.calculateIntervals(this.ConfigService.getConfigParam('sessionTimeout')); - this.showWarningInterval = intervals.showWarningInterval; - this.forceLogoutAfterWarningInterval = intervals.forceLogoutAfterWarningInterval; - this.checkMouseEventInterval = this.convertMinutesToMilliseconds(1); - this.updateLastActivityTimestamp(); - this.initializeListeners(); - this.initializeSession(); - } - - calculateIntervals(sessionTimeout) { - const forceLogoutAfterWarningInterval = Math.min( - sessionTimeout * 0.1, - this.defaultForceLogoutAfterWarningInterval - ); - const showWarningInterval = sessionTimeout - forceLogoutAfterWarningInterval; - return { - showWarningInterval: showWarningInterval, - forceLogoutAfterWarningInterval: forceLogoutAfterWarningInterval - }; - } - - initializeListeners() { - this.$rootScope.$on('goHome', () => { - this.goHome(); - }); - - this.$rootScope.$on('logOut', () => { - this.logOut(); - }); - } - - goHome() { - this.$rootScope.$broadcast('exit'); - this.$location.url(this.ConfigService.getConfigParam('userType')); - } - - logOut() { - window.location.href = this.ConfigService.getSessionLogOutURL(); - } - - initializeSession() { - if (!this.ConfigService.isPreview()) { - this.startCheckMouseEvent(); - } - } - - startCheckMouseEvent() { - setInterval(() => { - this.checkMouseEvent(); - }, this.checkMouseEventInterval); - } - - convertMinutesToSeconds(minutes) { - return minutes * 60; - } - - convertMinutesToMilliseconds(minutes) { - return minutes * 60 * 1000; - } - - /** - * Note: This does not get called when the warning popup is being shown. - */ - mouseMoved() { - this.updateLastActivityTimestamp(); - } - - updateLastActivityTimestamp() { - this.lastActivityTimestamp = new Date(); - } - - checkMouseEvent() { - if (this.isActiveWithinLastMinute()) { - this.renewSession(); - } else if (this.isInactiveLongEnoughToForceLogout()) { - this.forceLogOut(); - } else if (this.isInactiveLongEnoughToWarn() && !this.isShowingWarning()) { - this.showWarning(); - } - } - - isActiveWithinLastMinute() { - return new Date() - this.lastActivityTimestamp < this.convertMinutesToMilliseconds(1); - } - - isInactiveLongEnoughToForceLogout() { - return ( - this.getInactiveTimeInSeconds() >= - this.showWarningInterval + this.forceLogoutAfterWarningInterval - ); - } - - isInactiveLongEnoughToWarn() { - return this.getInactiveTimeInSeconds() >= this.showWarningInterval; - } - - isShowingWarning() { - return this.warningVisible; - } - - getInactiveTimeInSeconds() { - return Math.floor(this.getInactiveTimeInMilliseconds() / 1000); - } - - getInactiveTimeInMilliseconds() { - return new Date() - this.lastActivityTimestamp; - } - - forceLogOut() { - this.$rootScope.$broadcast('logOut'); - } - - showWarning() { - this.warningVisible = true; - this.$rootScope.$broadcast('showSessionWarning'); - } - - closeWarningAndRenewSession() { - this.warningVisible = false; - this.updateLastActivityTimestamp(); - this.renewSession(); - } - - renewSession() { - const renewSessionURL = this.ConfigService.getConfigParam('renewSessionURL'); - this.$http.get(renewSessionURL).then(result => { - if (result.data === 'false') { - this.logOut(); - } - }); - } -} - -SessionService.$inject = ['$http', '$location', '$rootScope', 'ConfigService']; - -export default SessionService; diff --git a/src/main/webapp/wise5/services/sessionService.ts b/src/main/webapp/wise5/services/sessionService.ts new file mode 100644 index 0000000000..9ae2d95418 --- /dev/null +++ b/src/main/webapp/wise5/services/sessionService.ts @@ -0,0 +1,154 @@ +'use strict'; + +import { Injectable } from '@angular/core'; +import { UpgradeModule } from '@angular/upgrade/static'; +import { HttpClient } from '@angular/common/http'; +import { ConfigService } from "./configService"; + +@Injectable() +export class SessionService { + private warningVisible: boolean = false; + private defaultForceLogoutAfterWarningInterval: number = this.convertMinutesToSeconds(5); + private forceLogoutAfterWarningInterval: number; + private showWarningInterval: number; + private checkMouseEventInterval: number; + private lastActivityTimestamp: number; + + constructor( + protected upgrade: UpgradeModule, + protected http: HttpClient, + protected ConfigService: ConfigService + ) { + const intervals: any = + this.calculateIntervals(this.ConfigService.getConfigParam('sessionTimeout')); + this.showWarningInterval = intervals.showWarningInterval; + this.forceLogoutAfterWarningInterval = intervals.forceLogoutAfterWarningInterval; + this.checkMouseEventInterval = this.convertMinutesToMilliseconds(1); + this.updateLastActivityTimestamp(); + this.initializeSession(); + } + + calculateIntervals(sessionTimeout): any { + const forceLogoutAfterWarningInterval: number = Math.min( + sessionTimeout * 0.1, + this.defaultForceLogoutAfterWarningInterval + ); + const showWarningInterval: number = sessionTimeout - forceLogoutAfterWarningInterval; + return { + showWarningInterval: showWarningInterval, + forceLogoutAfterWarningInterval: forceLogoutAfterWarningInterval + }; + } + + goHome() { + this.upgrade.$injector.get('$rootScope').$broadcast('exit'); + this.upgrade.$injector.get('$location').url( + this.ConfigService.getConfigParam('userType') + ); + } + + logOut() { + window.location.href = this.ConfigService.getSessionLogOutURL(); + } + + initializeSession() { + if (!this.ConfigService.isPreview()) { + this.startCheckMouseEvent(); + } + } + + startCheckMouseEvent() { + setInterval(() => { + this.checkMouseEvent(); + }, this.checkMouseEventInterval); + } + + convertMinutesToSeconds(minutes): number { + return minutes * 60; + } + + convertMinutesToMilliseconds(minutes): number { + return minutes * 60 * 1000; + } + + /** + * Note: This does not get called when the warning popup is being shown. + */ + mouseMoved() { + this.updateLastActivityTimestamp(); + } + + updateLastActivityTimestamp() { + this.lastActivityTimestamp = new Date().getTime(); + } + + checkMouseEvent() { + if (this.isActiveWithinLastMinute()) { + this.renewSession(); + } else { + this.checkForLogout(); + } + } + + checkForLogout() { + if (this.isInactiveLongEnoughToForceLogout()) { + this.forceLogOut(); + } else if (this.isInactiveLongEnoughToWarn() && !this.isShowingWarning()) { + this.showWarning(); + } + } + + isActiveWithinLastMinute(): boolean { + return ( + new Date().getTime() - this.lastActivityTimestamp < + this.convertMinutesToMilliseconds(1) + ); + } + + isInactiveLongEnoughToForceLogout(): boolean { + return ( + this.getInactiveTimeInSeconds() >= + this.showWarningInterval + this.forceLogoutAfterWarningInterval + ); + } + + isInactiveLongEnoughToWarn(): boolean { + return this.getInactiveTimeInSeconds() >= this.showWarningInterval; + } + + isShowingWarning(): boolean { + return this.warningVisible; + } + + getInactiveTimeInSeconds(): number { + return Math.floor(this.getInactiveTimeInMilliseconds() / 1000); + } + + getInactiveTimeInMilliseconds(): number { + return new Date().getTime() - this.lastActivityTimestamp; + } + + forceLogOut() { + this.upgrade.$injector.get('$rootScope').$broadcast('logOut'); + } + + showWarning() { + this.warningVisible = true; + this.upgrade.$injector.get('$rootScope').$broadcast('showSessionWarning'); + } + + closeWarningAndRenewSession() { + this.warningVisible = false; + this.updateLastActivityTimestamp(); + this.renewSession(); + } + + renewSession() { + const renewSessionURL = this.ConfigService.getConfigParam('renewSessionURL'); + this.http.get(renewSessionURL).toPromise().then(result => { + if (result === 'false') { + this.logOut(); + } + }); + } +} \ No newline at end of file diff --git a/src/main/webapp/wise5/services/studentAssetService.js b/src/main/webapp/wise5/services/studentAssetService.js deleted file mode 100644 index 10d9911808..0000000000 --- a/src/main/webapp/wise5/services/studentAssetService.js +++ /dev/null @@ -1,276 +0,0 @@ -'use strict'; - -class StudentAssetService { - constructor( - $filter, - $http, - $q, - Upload, - $rootScope, - ConfigService) { - this.$filter = $filter; - this.$http = $http; - this.$q = $q; - this.Upload = Upload; - this.$rootScope = $rootScope; - this.ConfigService = ConfigService; - this.$translate = this.$filter('translate'); - this.allAssets = []; // keep track of student's assets - } - - getAssetById(assetId) { - for (let asset of this.allAssets) { - if (asset.id === assetId) { - return asset; - } - } - return null; - }; - - retrieveAssets() { - if (this.ConfigService.isPreview()) { - // if we're in preview, don't make any request to the server but pretend we did - this.allAssets = []; - let deferred = this.$q.defer(); - deferred.resolve(this.allAssets); - return deferred.promise; - } else { - let config = { - method: "GET", - url: this.ConfigService.getStudentAssetsURL(), - params: { - workgroupId: this.ConfigService.getWorkgroupId() - } - }; - return this.$http(config).then((response) => { - // loop through the assets and make them into JSON object with more details - let result = []; - let assets = response.data; - let studentUploadsBaseURL = this.ConfigService.getStudentUploadsBaseURL(); - for (let asset of assets) { - if (!asset.isReferenced && asset.serverDeleteTime == null && asset.fileName !== '.DS_Store') { - asset.url = studentUploadsBaseURL + asset.filePath; - if (this.isImage(asset)) { - asset.type = 'image'; - asset.iconURL = asset.url; - } else if (this.isAudio(asset)) { - asset.type = 'audio'; - asset.iconURL = 'wise5/vle/notebook/audio.png'; - } else { - asset.type = 'file'; - asset.iconURL = 'wise5/vle/notebook/file.png'; - } - result.push(asset); - } - } - this.allAssets = result; - return result; - }); - } - }; - - getAssetContent(asset) { - const assetContentURL = asset.url; - - // retrieve the csv file and parse it - const config = {}; - config.method = 'GET'; - config.url = assetContentURL; - return this.$http(config).then((response) => { - return response.data; - }); - }; - - isImage(asset) { - const imageFileExtensions = ['png', 'jpg', 'jpeg', 'gif']; - if (asset != null) { - const assetURL = asset.url; - if (assetURL != null && assetURL.lastIndexOf('.') !== -1) { - const assetExtension = assetURL.substring(assetURL.lastIndexOf('.') + 1); - if (imageFileExtensions.indexOf(assetExtension.toLowerCase()) != -1) { - return true; - } - } - } - return false; - }; - - isAudio(asset) { - const imageFileExtensions = ['wav', 'mp3', 'ogg', 'm4a', 'm4p', 'raw', 'aiff']; - if (asset != null) { - const assetURL = asset.url; - if (assetURL != null && assetURL.lastIndexOf('.') != -1) { - const assetExtension = assetURL.substring(assetURL.lastIndexOf('.') + 1); - if (imageFileExtensions.indexOf(assetExtension.toLowerCase()) != -1) { - return true; - } - } - } - return false; - }; - - uploadAsset(file) { - if (this.ConfigService.isPreview()) { - return this.$q((resolve, reject) => { - const reader = new FileReader(); - - // Closure to capture the file information. - reader.onload = ( (theFile) => { - return (e) => { - let fileSrc = e.target.result; - let fileName = theFile.name; - - let asset = {}; - asset.file = file; - asset.url = fileSrc; - // assume this is an image for now. in the future, support audio and other file formats. - asset.type = 'image'; - asset.iconURL = asset.url; - - this.allAssets.push(asset); - this.$rootScope.$broadcast('studentAssetsUpdated'); - return resolve(asset); - }; - })(file); - - // Read in the image file as a data URL. - reader.readAsDataURL(file); - }); - } else { - const studentAssetsURL = this.ConfigService.getStudentAssetsURL(); - const deferred = this.$q.defer(); - - this.Upload.upload({ - url: studentAssetsURL, - fields: { - 'runId': this.ConfigService.getRunId(), - 'workgroupId': this.ConfigService.getWorkgroupId(), - 'periodId': this.ConfigService.getPeriodId(), - 'clientSaveTime': Date.parse(new Date()) - }, - file: file - }).success((asset, status, headers, config) => { - if (asset === "error") { - alert(this.$translate('THERE_WAS_AN_ERROR_UPLOADING')); - } else { - const studentUploadsBaseURL = this.ConfigService.getStudentUploadsBaseURL(); - asset.url = studentUploadsBaseURL + asset.filePath; - if (this.isImage(asset)) { - asset.type = 'image'; - asset.iconURL = asset.url; - } else if (this.isAudio(asset)) { - asset.type = 'audio'; - asset.iconURL = 'wise5/vle/notebook/audio.png'; - } else { - asset.type = 'file'; - asset.iconURL = 'wise5/vle/notebook/file.png'; - } - this.allAssets.push(asset); - this.$rootScope.$broadcast('studentAssetsUpdated'); - deferred.resolve(asset); - } - }).error((asset, status, headers, config) => { - alert(this.$translate('THERE_WAS_AN_ERROR_UPLOADING_YOU_MIGHT_HAVE_REACHED_LIMIT')); - }); - - return deferred.promise; - } - }; - - uploadAssets(files) { - const studentAssetsURL = this.ConfigService.getStudentAssetsURL(); - const promises = files.map((file) => { - return this.Upload.upload({ - url: studentAssetsURL, - fields: { - 'runId': this.ConfigService.getRunId(), - 'workgroupId': this.ConfigService.getWorkgroupId(), - 'periodId': this.ConfigService.getPeriodId(), - 'clientSaveTime': Date.parse(new Date()) - }, - file: file - }).progress((evt) => { - const progressPercentage = parseInt(100.0 * evt.loaded / evt.total); - //console.log('progress: ' + progressPercentage + '% ' + evt.config.file.name); - }).success((data, status, headers, config) => { - //console.log('file ' + config.file.name + 'uploaded. Response: ' + JSON.stringify(data)); - }); - }); - return this.$q.all(promises); - }; - - // given asset, makes a copy of it so steps can use for reference. Returns newly-copied asset. - copyAssetForReference(studentAsset) { - if (this.ConfigService.isPreview()) { - return this.$q((resolve, reject) => { - return resolve(studentAsset); - }); - } else { - const config = {}; - config.method = 'POST'; - config.url = this.ConfigService.getStudentAssetsURL() + '/copy'; - config.headers = {'Content-Type': 'application/x-www-form-urlencoded'}; - const params = {}; - params.studentAssetId = studentAsset.id; - params.workgroupId = this.ConfigService.getWorkgroupId(); - params.periodId = this.ConfigService.getPeriodId(); - params.clientSaveTime = Date.parse(new Date()); - - config.data = $.param(params); - - return this.$http(config).then((result) => { - const copiedAsset = result.data; - if (copiedAsset != null) { - const studentUploadsBaseURL = this.ConfigService.getStudentUploadsBaseURL(); - if (copiedAsset.isReferenced && copiedAsset.fileName !== '.DS_Store') { - copiedAsset.url = studentUploadsBaseURL + copiedAsset.filePath; - if (this.isImage(copiedAsset)) { - copiedAsset.type = 'image'; - copiedAsset.iconURL = copiedAsset.url; - } else if (this.isAudio(copiedAsset)) { - copiedAsset.type = 'audio'; - copiedAsset.iconURL = 'wise5/vle/notebook/audio.png'; - } else { - copiedAsset.type = 'file'; - copiedAsset.iconURL = 'wise5/vle/notebook/file.png'; - } - //this.$rootScope.$broadcast('studentAssetsUpdated'); - return copiedAsset; - } - } - return null; - }); - } - }; - - deleteAsset(studentAsset) { - const config = {}; - config.method = 'POST'; - config.url = this.ConfigService.getStudentAssetsURL() + '/remove'; - config.headers = {'Content-Type': 'application/x-www-form-urlencoded'}; - const params = {}; - params.studentAssetId = studentAsset.id; - params.workgroupId = this.ConfigService.getWorkgroupId(); - params.periodId = this.ConfigService.getPeriodId(); - params.clientDeleteTime = Date.parse(new Date()); - config.data = $.param(params); - - return this.$http(config).then((result) => { - //const deletedAsset = result.data; - // also remove from local copy of all assets - this.allAssets = this.allAssets.splice(this.allAssets.indexOf(studentAsset), 1); - this.$rootScope.$broadcast('studentAssetsUpdated'); - return studentAsset; - }); - }; -} - -StudentAssetService.$inject = [ - '$filter', - '$http', - '$q', - 'Upload', - '$rootScope', - 'ConfigService']; - -export default StudentAssetService; diff --git a/src/main/webapp/wise5/services/studentAssetService.ts b/src/main/webapp/wise5/services/studentAssetService.ts new file mode 100644 index 0000000000..2326c6a700 --- /dev/null +++ b/src/main/webapp/wise5/services/studentAssetService.ts @@ -0,0 +1,231 @@ +'use strict'; + +import { Injectable } from '@angular/core'; +import { UpgradeModule } from '@angular/upgrade/static'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { ConfigService } from './configService'; + +@Injectable() +export class StudentAssetService { + allAssets = []; + imageFileExtensions = ['png', 'jpg', 'jpeg', 'gif']; + audioFileExtensions = ['wav', 'mp3', 'ogg', 'm4a', 'm4p', 'raw', 'aiff', 'webm']; + + constructor(private upgrade: UpgradeModule, private http: HttpClient, + private ConfigService: ConfigService) { + } + + getAssetById(assetId) { + for (const asset of this.allAssets) { + if (asset.id === assetId) { + return asset; + } + } + return null; + } + + retrieveAssets() { + if (this.ConfigService.isPreview()) { + this.allAssets = []; + const deferred = this.upgrade.$injector.get('$q').defer(); + deferred.resolve(this.allAssets); + return deferred.promise; + } else { + const options = { + params: new HttpParams().set("workgroupId", this.ConfigService.getWorkgroupId()) + }; + return this.http.get(this.ConfigService.getStudentAssetsURL(), options) + .toPromise().then((assets: any) => { + // loop through the assets and make them into JSON object with more details + let result = []; + let studentUploadsBaseURL = this.ConfigService.getStudentUploadsBaseURL(); + for (const asset of assets) { + if (!asset.isReferenced && asset.serverDeleteTime == null && + asset.fileName !== '.DS_Store') { + asset.url = studentUploadsBaseURL + asset.filePath; + if (this.isImage(asset)) { + asset.type = 'image'; + asset.iconURL = asset.url; + } else if (this.isAudio(asset)) { + asset.type = 'audio'; + asset.iconURL = 'wise5/vle/notebook/audio.png'; + } else { + asset.type = 'file'; + asset.iconURL = 'wise5/vle/notebook/file.png'; + } + result.push(asset); + } + } + this.allAssets = result; + return result; + }); + } + } + + getAssetContent(asset) { + return this.http.get(asset.url).toPromise().then(response => { + return response; + }); + } + + hasSuffix(assetURL, suffixes) { + const assetExtension = assetURL.substring(assetURL.lastIndexOf('.') + 1); + return suffixes.includes(assetExtension.toLowerCase()); + } + + isImage(asset) { + return this.hasSuffix(this.getFileNameFromAsset(asset), this.imageFileExtensions); + } + + isAudio(asset) { + return this.hasSuffix(this.getFileNameFromAsset(asset), this.audioFileExtensions); + } + + getFileNameFromAsset(asset) { + if (this.ConfigService.isPreview()) { + return asset.file.name; + } else { + return asset.fileName; + } + } + + uploadAsset(file) { + if (this.ConfigService.isPreview()) { + return this.upgrade.$injector.get('$q')((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (theFile => { + return e => { + let fileSrc = e.target.result; + let fileName = theFile.name; + + let asset: any = {}; + asset.file = file; + asset.url = fileSrc; + if (this.isImage(asset)) { + asset.type = 'image'; + asset.iconURL = asset.url; + } else if (this.isAudio(asset)) { + asset.type = 'audio'; + asset.iconURL = 'wise5/themes/default/images/audio.png'; + } else { + asset.type = 'file'; + asset.iconURL = 'wise5/themes/default/images/file.png'; + } + this.allAssets.push(asset); + return resolve(asset); + }; + })(file); + reader.readAsDataURL(file); + }); + } else { + const deferred = this.upgrade.$injector.get('$q').defer(); + this.upgrade.$injector.get('Upload').upload({ + url: this.ConfigService.getStudentAssetsURL(), + fields: { + runId: this.ConfigService.getRunId(), + workgroupId: this.ConfigService.getWorkgroupId(), + periodId: this.ConfigService.getPeriodId(), + clientSaveTime: Date.parse(new Date().toString()) + }, + file: file + }) + .success((asset, status, headers, config) => { + if (asset === 'error') { + alert(this.upgrade.$injector.get('$filter')('translate')('THERE_WAS_AN_ERROR_UPLOADING')); + } else { + const studentUploadsBaseURL = this.ConfigService.getStudentUploadsBaseURL(); + asset.url = studentUploadsBaseURL + asset.filePath; + if (this.isImage(asset)) { + asset.type = 'image'; + asset.iconURL = asset.url; + } else if (this.isAudio(asset)) { + asset.type = 'audio'; + asset.iconURL = 'wise5/themes/default/images/audio.png'; + } else { + asset.type = 'file'; + asset.iconURL = 'wise5/themes/default/images/file.png'; + } + this.allAssets.push(asset); + deferred.resolve(asset); + } + }) + .error((asset, status, headers, config) => { + alert(this.upgrade.$injector.get('$filter')('translate')('THERE_WAS_AN_ERROR_UPLOADING_YOU_MIGHT_HAVE_REACHED_LIMIT')); + }); + return deferred.promise; + } + } + + uploadAssets(files) { + const promises = files.map(file => { + return this.upgrade.$injector.get('Upload').upload({ + url: this.ConfigService.getStudentAssetsURL(), + fields: { + runId: this.ConfigService.getRunId(), + workgroupId: this.ConfigService.getWorkgroupId(), + periodId: this.ConfigService.getPeriodId(), + clientSaveTime: Date.parse(new Date().toString()) + }, + file: file + }) + }); + return this.upgrade.$injector.get('$q').all(promises); + } + + // given asset, makes a copy of it so steps can use for reference. Returns newly-copied asset. + copyAssetForReference(studentAsset) { + if (this.ConfigService.isPreview()) { + return this.upgrade.$injector.get('$q')((resolve, reject) => { + return resolve(studentAsset); + }); + } else { + return this.http.post(`${this.ConfigService.getStudentAssetsURL()}/copy`, + { + studentAssetId: studentAsset.id, + workgroupId: this.ConfigService.getWorkgroupId(), + periodId: this.ConfigService.getPeriodId(), + clientSaveTime: Date.parse(new Date().toString()) + }) + .toPromise().then((copiedAsset: any) => { + if (copiedAsset != null) { + const studentUploadsBaseURL = this.ConfigService.getStudentUploadsBaseURL(); + if (copiedAsset.isReferenced && copiedAsset.fileName !== '.DS_Store') { + copiedAsset.url = studentUploadsBaseURL + copiedAsset.filePath; + if (this.isImage(copiedAsset)) { + copiedAsset.type = 'image'; + copiedAsset.iconURL = copiedAsset.url; + } else if (this.isAudio(copiedAsset)) { + copiedAsset.type = 'audio'; + copiedAsset.iconURL = 'wise5/vle/notebook/audio.png'; + } else { + copiedAsset.type = 'file'; + copiedAsset.iconURL = 'wise5/vle/notebook/file.png'; + } + return copiedAsset; + } + } + return null; + }); + } + } + + deleteAsset(studentAsset) { + if (this.ConfigService.isPreview()) { + return this.upgrade.$injector.get('$q')((resolve, reject) => { + this.allAssets = this.allAssets.splice(this.allAssets.indexOf(studentAsset), 1); + return resolve(studentAsset); + }); + } else { + return this.http.post(`${this.ConfigService.getStudentAssetsURL()}/delete`, + { + studentAssetId: studentAsset.id, + workgroupId: this.ConfigService.getWorkgroupId(), + periodId: this.ConfigService.getPeriodId(), + clientDeleteTime: Date.parse(new Date().toString()), + }).toPromise().then(() => { + this.allAssets = this.allAssets.splice(this.allAssets.indexOf(studentAsset), 1); + return studentAsset; + }); + } + } +} diff --git a/src/main/webapp/wise5/services/studentDataService.js b/src/main/webapp/wise5/services/studentDataService.js index 0fd30fd344..5bafc0ec5d 100644 --- a/src/main/webapp/wise5/services/studentDataService.js +++ b/src/main/webapp/wise5/services/studentDataService.js @@ -9,7 +9,6 @@ class StudentDataService { $rootScope, AnnotationService, ConfigService, - PlanningService, ProjectService, UtilService ) { @@ -20,7 +19,6 @@ class StudentDataService { this.$rootScope = $rootScope; this.AnnotationService = AnnotationService; this.ConfigService = ConfigService; - this.PlanningService = PlanningService; this.ProjectService = ProjectService; this.UtilService = UtilService; this.$translate = this.$filter('translate'); @@ -37,8 +35,6 @@ class StudentDataService { this.runStatus = null; this.maxScore = null; - this.maxPlanningNodeNumber = 0; - /* * A counter to keep track of how many saveToServer requests we have * made that we haven't received a response for yet. When this value @@ -814,7 +810,7 @@ class StudentDataService { return; } const context = 'VLE'; - this.saveEvent(context, nodeId, componentId, componentType, category, event, data); + return this.saveEvent(context, nodeId, componentId, componentType, category, event, data); } saveEvent(context, nodeId, componentId, componentType, category, event, data) { @@ -831,7 +827,7 @@ class StudentDataService { events.push(newEvent); const componentStates = undefined; const annotations = undefined; - this.saveToServer(componentStates, events, annotations); + return this.saveToServer(componentStates, events, annotations); } createNewEvent(nodeId, componentId, context, componentType, category, event, data) { @@ -1661,7 +1657,6 @@ StudentDataService.$inject = [ '$rootScope', 'AnnotationService', 'ConfigService', - 'PlanningService', 'ProjectService', 'UtilService' ]; diff --git a/src/main/webapp/wise5/test-unit/services/sessionService.spec.js b/src/main/webapp/wise5/test-unit/services/sessionService.spec.js deleted file mode 100644 index 3d6a72a3b3..0000000000 --- a/src/main/webapp/wise5/test-unit/services/sessionService.spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import vleModule from '../../vle/vle'; - -let SessionService; - -describe('SessionService', () => { - beforeEach(angular.mock.module(vleModule.name)); - - beforeEach(inject(_SessionService_ => { - SessionService = _SessionService_; - })); - - describe('calculateIntervals()', () => { - shouldCalculateTheWarnAndLogoutIntervalsWhenSessionTimeoutIs10Minutes(); - shouldCalculateTheWarnAndLogoutIntervalsWhenSessionTimeoutIs30Minutes(); - shouldCalculateTheWarnAndLogoutIntervalsWhenSessionTimeoutIs60Minutes(); - }); -}); - -function shouldCalculateTheWarnAndLogoutIntervalsWhenSessionTimeoutIs10Minutes() { - it('should calculate the warn and logout intervals when session timeout is 10 minutes', () => { - const sessionTimeout = 600; - const intervals = SessionService.calculateIntervals(sessionTimeout); - expect(intervals.showWarningInterval).toEqual(540); - expect(intervals.forceLogoutAfterWarningInterval).toEqual(60); - }); -} - -function shouldCalculateTheWarnAndLogoutIntervalsWhenSessionTimeoutIs30Minutes() { - it('should calculate the warn and logout intervals when session timeout is 30 minutes', () => { - const sessionTimeout = 1800; - const intervals = SessionService.calculateIntervals(sessionTimeout); - expect(intervals.showWarningInterval).toEqual(1620); - expect(intervals.forceLogoutAfterWarningInterval).toEqual(180); - }); -} - -function shouldCalculateTheWarnAndLogoutIntervalsWhenSessionTimeoutIs60Minutes() { - it('should calculate the warn and logout intervals when session timeout is 60 minutes', () => { - const sessionTimeout = 3600; - const intervals = SessionService.calculateIntervals(sessionTimeout); - expect(intervals.showWarningInterval).toEqual(3300); - expect(intervals.forceLogoutAfterWarningInterval).toEqual(300); - }); -} diff --git a/src/main/webapp/wise5/themes/default/navigation/navigation.html b/src/main/webapp/wise5/themes/default/navigation/navigation.html index 34c0a5b825..a0b085f580 100644 --- a/src/main/webapp/wise5/themes/default/navigation/navigation.html +++ b/src/main/webapp/wise5/themes/default/navigation/navigation.html @@ -1,4 +1,3 @@ -