diff --git a/package-lock.json b/package-lock.json index 6d53a740ec..893b665802 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "wise", - "version": "5.18.4", + "version": "5.19.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 23d097c2a1..a67eb54415 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wise", - "version": "5.18.4", + "version": "5.19.0", "description": "Web-based Inquiry Science Environment", "main": "app.js", "browserslist": [ diff --git a/pom.xml b/pom.xml index aacbbc7e07..f0dacb03ff 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ wise war Web-based Inquiry Science Environment - 5.18.4 + 5.19.0 http://wise5.org diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/DiscourseSSOController.java b/src/main/java/org/wise/portal/presentation/web/controllers/DiscourseSSOController.java new file mode 100644 index 0000000000..93d1c766e5 --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/controllers/DiscourseSSOController.java @@ -0,0 +1,98 @@ +package org.wise.portal.presentation.web.controllers; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Properties; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.view.RedirectView; +import org.wise.portal.domain.authentication.MutableUserDetails; +import org.wise.portal.domain.user.User; +import org.wise.portal.presentation.util.http.Base64; +import org.wise.portal.service.user.UserService; + +@Controller +public class DiscourseSSOController { + + @Autowired + Properties appProperties; + + @Autowired + UserService userService; + + @GetMapping("/sso/discourse") + protected RedirectView discourseSSOLogin(@RequestParam("sso") String base64EncodedSSO, + @RequestParam("sig") String sigParam, Authentication auth) throws Exception { + String secretKey = appProperties.getProperty("discourse_sso_secret_key"); + String discourseURL = appProperties.getProperty("discourse_url"); + if (secretKey == null || secretKey.isEmpty() || discourseURL == null || discourseURL.isEmpty()) { + return null; + } + String base64DecodedSSO = new String(Base64.decode(base64EncodedSSO), "UTF-8"); + if (!base64DecodedSSO.startsWith("nonce=")) { + return null; + } + String nonce = base64DecodedSSO.substring(6); + String algorithm = "HmacSHA256"; + String hMACSHA256Message = hmacDigest(base64EncodedSSO, secretKey, algorithm); + if (!hMACSHA256Message.equals(sigParam)) { + return null; + } + User user = userService.retrieveUserByUsername(auth.getName()); + return new RedirectView( + generateDiscourseSSOLoginURL(secretKey, discourseURL, nonce, algorithm, user)); + } + + private String generateDiscourseSSOLoginURL(String secretKey, String discourseURL, String nonce, + String algorithm, User user) throws UnsupportedEncodingException { + String wiseUserId = URLEncoder.encode(user.getId().toString(), "UTF-8"); + MutableUserDetails userDetails = user.getUserDetails(); + String username = URLEncoder.encode(userDetails.getUsername(), "UTF-8"); + String name = + URLEncoder.encode(userDetails.getFirstname() + " " + userDetails.getLastname(), "UTF-8"); + String email = URLEncoder.encode(userDetails.getEmailAddress(), "UTF-8"); + String payLoadString = "nonce=" + nonce + "&name=" + name + "&username=" + username + + "&email=" + email + "&external_id=" + wiseUserId; + String payLoadStringBase64Encoded = Base64.encodeBytes(payLoadString.getBytes()); + String payLoadStringBase64EncodedURLEncoded = + URLEncoder.encode(payLoadStringBase64Encoded, "UTF-8"); + String payLoadStringBase64EncodedHMACSHA256Signed = + hmacDigest(payLoadStringBase64Encoded, secretKey, algorithm); + String discourseSSOLoginURL = discourseURL + "/session/sso_login" + + "?sso=" + payLoadStringBase64EncodedURLEncoded + + "&sig=" + payLoadStringBase64EncodedHMACSHA256Signed; + return discourseSSOLoginURL; + } + + public static String hmacDigest(String msg, String secretKey, String algorithm) { + String digest = null; + try { + SecretKeySpec key = new SecretKeySpec((secretKey).getBytes("UTF-8"), algorithm); + Mac mac = Mac.getInstance(algorithm); + mac.init(key); + byte[] bytes = mac.doFinal(msg.getBytes("ASCII")); + StringBuffer hash = new StringBuffer(); + for (int i = 0; i < bytes.length; i++) { + String hex = Integer.toHexString(0xFF & bytes[i]); + if (hex.length() == 1) { + hash.append('0'); + } + hash.append(hex); + } + digest = hash.toString(); + } catch (UnsupportedEncodingException e) { + } catch (InvalidKeyException e) { + } catch (NoSuchAlgorithmException e) { + } + return digest; + } +} diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java index 633d4ace40..b6ef0251f3 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/user/UserAPIController.java @@ -135,6 +135,7 @@ protected HashMap getConfig(HttpServletRequest request) { config.put("recaptchaPublicKey", appProperties.get("recaptcha_public_key")); config.put("wiseHostname", appProperties.get("wise.hostname")); config.put("wise4Hostname", appProperties.get("wise4.hostname")); + config.put("discourseURL", appProperties.getOrDefault("discourse_url", null)); return config; } diff --git a/src/main/java/org/wise/portal/service/vle/wise5/VLEService.java b/src/main/java/org/wise/portal/service/vle/wise5/VLEService.java index 45e067430a..268a259f23 100644 --- a/src/main/java/org/wise/portal/service/vle/wise5/VLEService.java +++ b/src/main/java/org/wise/portal/service/vle/wise5/VLEService.java @@ -122,12 +122,7 @@ Annotation saveAnnotation(Integer id, Integer runId, Integer periodId, Integer f String localNotebookItemId, Integer notebookItemId, String type, String data, String clientSaveTime) throws ObjectNotFoundException; - /** - * @return StudentsAssets from data store - */ - List getStudentAssets(Integer id, Integer runId, Integer periodId, - Integer workgroupId, String nodeId, String componentId, String componentType, - Boolean isReferenced) throws ObjectNotFoundException; + List getWorkgroupAssets(Long workgroupId) throws ObjectNotFoundException; /** * Saves StudentAssets in the data store 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 7d42e46b25..2406bfcc24 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 @@ -620,35 +620,10 @@ public Annotation saveAnnotation(Integer id, Integer runId, Integer periodId, } @Override - public List getStudentAssets(Integer id, Integer runId, Integer periodId, - Integer workgroupId, String nodeId, String componentId, String componentType, - Boolean isReferenced) { - Run run = null; - if (runId != null) { - try { - run = runService.retrieveById(new Long(runId)); - } catch (ObjectNotFoundException e) { - e.printStackTrace(); - } - } - Group period = null; - if (periodId != null) { - try { - period = groupService.retrieveById(new Long(periodId)); - } catch (ObjectNotFoundException e) { - e.printStackTrace(); - } - } - Workgroup workgroup = null; - if (workgroupId != null) { - try { - workgroup = workgroupService.retrieveById(new Long(workgroupId)); - } catch (ObjectNotFoundException e) { - e.printStackTrace(); - } - } - return studentAssetDao.getStudentAssetListByParams(id, run, period, workgroup, nodeId, - componentId, componentType, isReferenced); + public List getWorkgroupAssets(Long workgroupId) throws ObjectNotFoundException { + Workgroup workgroup = workgroupService.retrieveById(workgroupId); + return studentAssetDao.getStudentAssetListByParams(null, null, null, workgroup, null, + null, null, null); } @Override diff --git a/src/main/java/org/wise/portal/service/work/AchievementJsonModule.java b/src/main/java/org/wise/portal/service/work/AchievementJsonModule.java new file mode 100644 index 0000000000..8469792667 --- /dev/null +++ b/src/main/java/org/wise/portal/service/work/AchievementJsonModule.java @@ -0,0 +1,21 @@ +package org.wise.portal.service.work; + +import com.fasterxml.jackson.databind.module.SimpleModule; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.wise.vle.domain.achievement.Achievement; +import org.wise.vle.domain.achievement.AchievementSerializer; + +@Service +public class AchievementJsonModule extends SimpleModule { + + private static final long serialVersionUID = 1L; + + public AchievementJsonModule() {} + + @Autowired + public AchievementJsonModule(AchievementSerializer serializer) { + this.addSerializer(Achievement.class, serializer); + } +} diff --git a/src/main/java/org/wise/portal/service/work/StudentAssetJsonModule.java b/src/main/java/org/wise/portal/service/work/StudentAssetJsonModule.java new file mode 100644 index 0000000000..44be8254b8 --- /dev/null +++ b/src/main/java/org/wise/portal/service/work/StudentAssetJsonModule.java @@ -0,0 +1,21 @@ +package org.wise.portal.service.work; + +import com.fasterxml.jackson.databind.module.SimpleModule; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.wise.vle.domain.work.StudentAsset; +import org.wise.vle.domain.work.StudentAssetSerializer; + +@Service +public class StudentAssetJsonModule extends SimpleModule { + + private static final long serialVersionUID = 1L; + + public StudentAssetJsonModule() {} + + @Autowired + public StudentAssetJsonModule(StudentAssetSerializer serializer) { + this.addSerializer(StudentAsset.class, serializer); + } +} diff --git a/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java b/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java index e2a7392599..d792511379 100644 --- a/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java +++ b/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java @@ -94,6 +94,7 @@ protected void configure(HttpSecurity http) throws Exception { .antMatchers("/student/**").hasAnyRole("STUDENT") .antMatchers("/studentStatus").hasAnyRole("TEACHER,STUDENT") .antMatchers("/teacher/**").hasAnyRole("TEACHER") + .antMatchers("/sso/discourse").hasAnyRole("TEACHER,STUDENT") .antMatchers("/").permitAll(); http.formLogin().loginPage("/login").permitAll(); http.sessionManagement().maximumSessions(2).sessionRegistry(sessionRegistry()); @@ -182,7 +183,7 @@ public AuthenticationSuccessHandler authSuccessHandler() { @Bean public AuthenticationFailureHandler authFailureHandler() { WISEAuthenticationFailureHandler handler = new WISEAuthenticationFailureHandler(); - handler.setAuthenticationFailureUrl("/legacy/login?failed=true"); + handler.setAuthenticationFailureUrl("/login?failed=true"); return handler; } diff --git a/src/main/java/org/wise/vle/domain/achievement/Achievement.java b/src/main/java/org/wise/vle/domain/achievement/Achievement.java index 7148c63d31..095cdacd9b 100644 --- a/src/main/java/org/wise/vle/domain/achievement/Achievement.java +++ b/src/main/java/org/wise/vle/domain/achievement/Achievement.java @@ -96,40 +96,19 @@ public void convertToClientAchievement() { public JSONObject toJSON() { JSONObject achievementJSONObject = new JSONObject(); - try { if (id != null) { achievementJSONObject.put("id", id); } - - if (run != null) { - Long runId = run.getId(); - achievementJSONObject.put("runId", runId); - } - - if (workgroup != null) { - Long workgroupId = workgroup.getId(); - achievementJSONObject.put("workgroupId", workgroupId); - } - - if (achievementId != null) { - achievementJSONObject.put("achievementId", achievementId); - } - - if (type != null) { - achievementJSONObject.put("type", type); - } - - if (achievementTime != null) { - achievementJSONObject.put("achievementTime", achievementTime.getTime()); - } - - if (data != null) { - try { - achievementJSONObject.put("data", new JSONObject(data)); - } catch (JSONException e) { - achievementJSONObject.put("data", data); - } + achievementJSONObject.put("runId", run.getId()); + achievementJSONObject.put("workgroupId", workgroup.getId()); + achievementJSONObject.put("achievementId", achievementId); + achievementJSONObject.put("type", type); + achievementJSONObject.put("achievementTime", achievementTime.getTime()); + try { + achievementJSONObject.put("data", new JSONObject(data)); + } catch (JSONException e) { + achievementJSONObject.put("data", data); } } catch (JSONException e) { e.printStackTrace(); diff --git a/src/main/java/org/wise/vle/domain/achievement/AchievementSerializer.java b/src/main/java/org/wise/vle/domain/achievement/AchievementSerializer.java new file mode 100644 index 0000000000..140f2d9e05 --- /dev/null +++ b/src/main/java/org/wise/vle/domain/achievement/AchievementSerializer.java @@ -0,0 +1,30 @@ +package org.wise.vle.domain.achievement; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; + +import org.springframework.stereotype.Service; + +@Service +public class AchievementSerializer extends JsonSerializer { + + @Override + public void serialize(Achievement achievement, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeStartObject(); + gen.writeObjectField("id", achievement.getId()); + gen.writeObjectField("runId", achievement.getRun().getId()); + gen.writeObjectField("workgroupId", achievement.getWorkgroup().getId()); + gen.writeObjectField("achievementId", achievement.getAchievementId()); + gen.writeObjectField("type", achievement.getType()); + gen.writeObjectField("achievementTime", achievement.getAchievementTime().getTime()); + String data = achievement.getData(); + ObjectMapper mapper = new ObjectMapper(); + gen.writeObjectField("data", mapper.readTree(data)); + gen.writeEndObject(); + } +} diff --git a/src/main/java/org/wise/vle/domain/work/StudentAsset.java b/src/main/java/org/wise/vle/domain/work/StudentAsset.java index 793b60097f..e45043b163 100644 --- a/src/main/java/org/wise/vle/domain/work/StudentAsset.java +++ b/src/main/java/org/wise/vle/domain/work/StudentAsset.java @@ -23,10 +23,20 @@ */ package org.wise.vle.domain.work; -import lombok.Getter; -import lombok.Setter; -import org.json.JSONException; -import org.json.JSONObject; +import java.sql.Timestamp; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + import org.wise.portal.domain.group.Group; import org.wise.portal.domain.group.impl.PersistentGroup; import org.wise.portal.domain.run.Run; @@ -35,8 +45,8 @@ import org.wise.portal.domain.workgroup.impl.WorkgroupImpl; import org.wise.vle.domain.PersistableDomain; -import javax.persistence.*; -import java.sql.Timestamp; +import lombok.Getter; +import lombok.Setter; /** * Domain object representing assets uploaded by the student like images and video (used in WISE5) @@ -103,79 +113,4 @@ public class StudentAsset extends PersistableDomain { protected Class> getObjectClass() { return StudentAsset.class; } - - /** - * Get the JSON representation of the StudentWork - * @return a JSONObject with the values from the StudentWork - */ - public JSONObject toJSON() { - JSONObject studentWorkJSONObject = new JSONObject(); - try { - if (id != null) { - studentWorkJSONObject.put("id", id); - } - - if (run != null) { - Long runId = run.getId(); - studentWorkJSONObject.put("runId", runId); - } - - if (period != null) { - Long periodId = period.getId(); - studentWorkJSONObject.put("periodId", periodId); - } - - if (workgroup != null) { - Long workgroupId = workgroup.getId(); - studentWorkJSONObject.put("workgroupId", workgroupId); - } - - if (nodeId != null) { - studentWorkJSONObject.put("nodeId", nodeId); - } - - if (componentId != null) { - studentWorkJSONObject.put("componentId", componentId); - } - - if (componentType != null) { - studentWorkJSONObject.put("componentType", componentType); - } - - if (isReferenced != null) { - studentWorkJSONObject.put("isReferenced", isReferenced); - } - - if (fileName != null) { - studentWorkJSONObject.put("fileName", fileName); - } - - if (filePath != null) { - studentWorkJSONObject.put("filePath", filePath); - } - - if (fileSize != null) { - studentWorkJSONObject.put("fileSize", fileSize); - } - - if (clientSaveTime != null) { - studentWorkJSONObject.put("clientSaveTime", clientSaveTime.getTime()); - } - - if (serverSaveTime != null) { - studentWorkJSONObject.put("serverSaveTime", serverSaveTime.getTime()); - } - - if (clientDeleteTime != null) { - studentWorkJSONObject.put("clientDeleteTime", clientDeleteTime.getTime()); - } - - if (serverDeleteTime != null) { - studentWorkJSONObject.put("serverDeleteTime", serverDeleteTime.getTime()); - } - } catch (JSONException e) { - e.printStackTrace(); - } - return studentWorkJSONObject; - } } diff --git a/src/main/java/org/wise/vle/domain/work/StudentAssetSerializer.java b/src/main/java/org/wise/vle/domain/work/StudentAssetSerializer.java new file mode 100644 index 0000000000..73e200620d --- /dev/null +++ b/src/main/java/org/wise/vle/domain/work/StudentAssetSerializer.java @@ -0,0 +1,37 @@ +package org.wise.vle.domain.work; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import org.springframework.stereotype.Service; + +@Service +public class StudentAssetSerializer extends JsonSerializer { + + @Override + public void serialize(StudentAsset asset, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeStartObject(); + gen.writeObjectField("id", asset.getId()); + gen.writeObjectField("runId", asset.getRun().getId()); + gen.writeObjectField("periodId", asset.getPeriod().getId()); + gen.writeObjectField("workgroupId", asset.getWorkgroup().getId()); + gen.writeObjectField("nodeId", asset.getNodeId()); + gen.writeObjectField("componentId", asset.getComponentId()); + gen.writeObjectField("componentType", asset.getComponentType()); + gen.writeObjectField("isReferenced", asset.getIsReferenced()); + gen.writeObjectField("fileName", asset.getFileName()); + gen.writeObjectField("filePath", asset.getFilePath()); + gen.writeObjectField("fileSize", asset.getFileSize()); + gen.writeObjectField("clientSaveTime", asset.getClientSaveTime().getTime()); + gen.writeObjectField("serverSaveTime", asset.getServerSaveTime().getTime()); + if (asset.getClientDeleteTime() != null) { + gen.writeObjectField("clientDeleteTime", asset.getClientDeleteTime().getTime()); + gen.writeObjectField("serverDeleteTime", asset.getServerDeleteTime().getTime()); + } + gen.writeEndObject(); + } +} diff --git a/src/main/java/org/wise/vle/web/wise5/AchievementController.java b/src/main/java/org/wise/vle/web/wise5/AchievementController.java new file mode 100644 index 0000000000..9adbe9ac44 --- /dev/null +++ b/src/main/java/org/wise/vle/web/wise5/AchievementController.java @@ -0,0 +1,118 @@ +package org.wise.vle.web.wise5; + +import java.io.IOException; +import java.util.List; + +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.wise.portal.dao.ObjectNotFoundException; +import org.wise.portal.domain.run.Run; +import org.wise.portal.domain.user.User; +import org.wise.portal.domain.workgroup.Workgroup; +import org.wise.portal.service.run.RunService; +import org.wise.portal.service.user.UserService; +import org.wise.portal.service.vle.wise5.VLEService; +import org.wise.portal.service.workgroup.WorkgroupService; +import org.wise.portal.spring.data.redis.MessagePublisher; +import org.wise.vle.domain.achievement.Achievement; + +@RestController +public class AchievementController { + + @Autowired + private MessagePublisher redisPublisher; + + @Autowired + private RunService runService; + + @Autowired + private UserService userService; + + @Autowired + private VLEService vleService; + + @Autowired + private WorkgroupService workgroupService; + + @GetMapping("/achievement/{runId}") + public List getWISE5StudentAchievements(@PathVariable Integer runId, + @RequestParam(value = "id", required = false) Integer id, + @RequestParam(value = "workgroupId", required = false) Integer workgroupId, + @RequestParam(value = "achievementId", required = false) String achievementId, + @RequestParam(value = "type", required = false) String type, + Authentication auth) throws ObjectNotFoundException { + User user = userService.retrieveUserByUsername(auth.getName()); + Workgroup workgroup = null; + Run run = runService.retrieveById(new Long(runId)); + if (workgroupId != null) { + workgroup = workgroupService.retrieveById(new Long(workgroupId)); + } + if (!isAssociatedWithRun(run, user, workgroup)) { + throw new AccessDeniedException("Not associated with run"); + } + return vleService.getAchievements(id, runId, workgroupId, achievementId, type); + } + + private Boolean isAssociatedWithRun(Run run, User user, Workgroup workgroup) { + return isStudentAssociatedWithRun(run, user, workgroup) + || isTeacherAssociatedWithRun(run, user); + } + + private Boolean isStudentAssociatedWithRun(Run run, User user, Workgroup workgroup) { + return user.isStudent() && run.isStudentAssociatedToThisRun(user) + && workgroupService.isUserInWorkgroupForRun(user, run, workgroup); + } + + private Boolean isTeacherAssociatedWithRun(Run run, User user) { + return (user.isTeacher() && run.isTeacherAssociatedToThisRun(user)) || user.isAdmin(); + } + + @PostMapping("/achievement/{runId}") + public Achievement saveAchievement(@PathVariable Integer runId, + @RequestParam(value = "id", required = false) Integer id, + @RequestParam(value = "workgroupId", required = true) Integer workgroupId, + @RequestParam(value = "achievementId", required = true) String achievementId, + @RequestParam(value = "type", required = true) String type, + @RequestParam(value = "data", required = true) String data, + Authentication auth) throws JSONException, ObjectNotFoundException, IOException { + User user = userService.retrieveUserByUsername(auth.getName()); + Run run = runService.retrieveById(new Long(runId)); + Workgroup workgroup = workgroupService.retrieveById(new Long(workgroupId)); + if (isAllowedToSave(run, user, workgroup)) { + Achievement achievement = vleService.saveAchievement(id, runId, workgroupId, achievementId, + type, data); + achievement.convertToClientAchievement(); + broadcastAchievementToTeacher(achievement); + return achievement; + } + throw new AccessDeniedException("Not allowed to save achievement"); + } + + private boolean isAllowedToSave(Run run, User user, Workgroup workgroup) { + if (user.isStudent() && run.isStudentAssociatedToThisRun(user) && + workgroupService.isUserInWorkgroupForRun(user, run, workgroup)) { + return true; + } else if (user.isTeacher() && + (run.getOwner().equals(user) || run.getSharedowners().contains(user))) { + return true; + } else { + return false; + } + } + + public void broadcastAchievementToTeacher(Achievement achievement) throws JSONException { + JSONObject message = new JSONObject(); + message.put("type", "achievementToTeacher"); + message.put("topic", String.format("/topic/teacher/%s", achievement.getRunId())); + message.put("achievement", achievement.toJSON()); + redisPublisher.publish(message.toString()); + } +} 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 7a758e8291..5031d6f76d 100644 --- a/src/main/java/org/wise/vle/web/wise5/StudentAssetController.java +++ b/src/main/java/org/wise/vle/web/wise5/StudentAssetController.java @@ -35,15 +35,15 @@ import com.fasterxml.jackson.databind.node.ObjectNode; -import org.json.JSONArray; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; 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; @@ -54,13 +54,15 @@ import org.wise.portal.domain.workgroup.Workgroup; import org.wise.portal.presentation.web.controllers.ControllerUtil; import org.wise.portal.service.run.RunService; +import org.wise.portal.service.user.UserService; import org.wise.portal.service.vle.wise5.VLEService; import org.wise.portal.service.workgroup.WorkgroupService; import org.wise.vle.domain.work.StudentAsset; import org.wise.vle.web.AssetManager; /** - * Controller for handling GET and POST requests of WISE5 StudentAsset domain objects + * REST endpoint for StudentAsset + * * @author Hiroki Terashima */ @Controller @@ -78,86 +80,33 @@ public class StudentAssetController { @Autowired private WorkgroupService workgroupService; - @GetMapping("/student/asset/{runId}") - protected void getStudentAssets( - @PathVariable Integer runId, - @RequestParam(value = "id", required = false) Integer id, - @RequestParam(value = "periodId", required = false) Integer periodId, - @RequestParam(value = "workgroupId", required = false) Integer workgroupId, - @RequestParam(value = "workgroups", required = false) String workgroups, - @RequestParam(value = "nodeId", required = false) String nodeId, - @RequestParam(value = "componentId", required = false) String componentId, - @RequestParam(value = "componentType", required = false) String componentType, - @RequestParam(value = "isReferenced", required = false) Boolean isReferenced, - HttpServletResponse response) throws IOException { - Run run = null; - try { - run = runService.retrieveById(new Long(runId)); - } catch (NumberFormatException e) { - e.printStackTrace(); - } catch (ObjectNotFoundException e) { - e.printStackTrace(); - } - String studentUploadsBaseDir = appProperties.getProperty("studentuploads_base_dir"); - if (workgroups != null) { - // this is a request from the teacher of the run or admin who wants to see the run's students' assets - /* COMMENTED OUT FOR NOW. This block will work, but does not use the StudentAsset domain object. - if (user.isAdmin() || runService.hasRunPermission(run, user, BasePermission.READ)) { // verify that user is the owner of the run - String[] workgroupIds = workgroups.split(":"); - JSONArray workgroupAssetLists = new JSONArray(); - for (int i = 0; i < workgroupIds.length; i++) { - String workgroupId = workgroupIds[i]; - JSONObject workgroupAsset = new JSONObject(); - try { - //get the directory name for the workgroup for this run - String dirName = run.getId() + "/" + workgroupId + "/unreferenced"; // looks like /studentuploads/[runId]/[workgroupId]/unreferenced + @Autowired + private UserService userService; - //get a list of file names in this workgroup's upload directory - JSONArray assetList = AssetManager.getAssetList(studentUploadsBaseDir, dirName); - workgroupAsset.put("workgroupId", workgroupId); - workgroupAsset.put("assets", assetList); - workgroupAssetLists.put(workgroupAsset); - } catch (NumberFormatException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (JSONException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - response.getWriter().write(workgroupAssetLists.toString()); - } - */ - } else if (workgroupId != null) { - try { - List studentAssets = vleService.getStudentAssets(id, runId, periodId, - workgroupId, nodeId, componentId, componentType, isReferenced); - JSONArray studentAssetList = new JSONArray(); - for (StudentAsset studentAsset : studentAssets) { - studentAssetList.put(studentAsset.toJSON()); - } - response.getWriter().write(studentAssetList.toString()); - } catch (ObjectNotFoundException e) { - e.printStackTrace(); - } + @GetMapping("/student/asset/{runId}/{workgroupId}") + @ResponseBody + protected List getWorkgroupAssets(@PathVariable Long runId, + @PathVariable Long workgroupId, Authentication auth) + throws IOException, ObjectNotFoundException { + User user = userService.retrieveUserByUsername(auth.getName()); + Run run = runService.retrieveById(runId); + Workgroup workgroup = workgroupService.retrieveById(workgroupId); + if (workgroupService.isUserInWorkgroupForRun(user, run, workgroup)) { + return vleService.getWorkgroupAssets(workgroupId); } + throw new AccessDeniedException("Access Denied"); } - /** - * Saves POSTed file into logged-in user's asset folder in the filesystem and in the database - */ @PostMapping("/student/asset/{runId}") - protected void postStudentAsset( - @PathVariable Integer runId, + @ResponseBody + protected StudentAsset postStudentAsset(@PathVariable Integer runId, @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, - HttpServletRequest request, - HttpServletResponse response) throws IOException { - + HttpServletRequest request) throws Exception { Run run = null; try { run = runService.retrieveById(new Long(runId)); @@ -169,8 +118,10 @@ protected void postStudentAsset( String dirName = run.getId() + "/" + workgroupId + "/unreferenced"; String path = appProperties.getProperty("studentuploads_base_dir"); - Long studentMaxAssetSize = new Long(appProperties.getProperty("student_max_asset_size", "5242880")); - Long studentMaxTotalAssetsSize = new Long(appProperties.getProperty("student_max_total_assets_size", "10485760")); + Long studentMaxAssetSize = new Long( + appProperties.getProperty("student_max_asset_size", "5242880")); + Long studentMaxTotalAssetsSize = new Long( + appProperties.getProperty("student_max_total_assets_size", "10485760")); String pathToCheckSize = path + "/" + dirName; StandardMultipartHttpServletRequest multiRequest = (StandardMultipartHttpServletRequest) request; Map fileMap = multiRequest.getFileMap(); @@ -181,61 +132,56 @@ protected void postStudentAsset( String key = iter.next(); MultipartFile file = fileMap.get(key); if (file.getSize() > studentMaxAssetSize) { - response.sendError(500, "error handling uploaded asset: filesize exceeds max allowed"); - return; + throw new Exception("error handling uploaded asset: filesize exceeds max allowed"); } String clientDeleteTime = null; - Boolean result = AssetManager.uploadAssetWISE5(file, path, dirName, pathToCheckSize, studentMaxTotalAssetsSize); + Boolean result = AssetManager.uploadAssetWISE5(file, path, dirName, pathToCheckSize, + studentMaxTotalAssetsSize); if (result) { Integer id = null; Boolean isReferenced = false; String fileName = file.getOriginalFilename(); String filePath = "/" + dirName + "/" + fileName; Long fileSize = file.getSize(); - - StudentAsset studentAsset = null; try { - studentAsset = vleService.saveStudentAsset(id, runId, periodId, workgroupId, nodeId, + return vleService.saveStudentAsset(id, runId, periodId, workgroupId, nodeId, componentId, componentType, isReferenced, fileName, filePath, fileSize, clientSaveTime, clientDeleteTime); - response.getWriter().write(studentAsset.toJSON().toString()); } catch (ObjectNotFoundException e) { e.printStackTrace(); - response.sendError(500, "error handling uploaded asset"); - return; + throw new Exception("error handling uploaded asset"); } } else { - response.sendError(500, "error: total asset size exceeds max allowed"); - return; + throw new Exception("error: total asset size exceeds max allowed"); } } } + return null; } - @PostMapping("/student/asset/{runId}/delete") + @DeleteMapping("/student/asset/{runId}/delete") @ResponseBody - protected String removeStudentAsset(@PathVariable Integer runId, - @RequestBody ObjectNode postedParams) throws IOException, ObjectNotFoundException { + protected StudentAsset 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) + throws Exception { 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 dirName = run.getId() + "/" + workgroupId + "/unreferenced"; String path = appProperties.getProperty("studentuploads_base_dir"); Boolean removeSuccess = AssetManager.removeAssetWISE5(path, dirName, assetFileName); if (removeSuccess) { - studentAsset = vleService.deleteStudentAsset(studentAssetId, clientDeleteTime); - return studentAsset.toJSON().toString(); + return vleService.deleteStudentAsset(studentAssetId, clientDeleteTime); } - return "error"; + throw new Exception("Error occurred"); } @PostMapping("/student/asset/{runId}/copy") @ResponseBody - protected String copyStudentAsset(@PathVariable Integer runId, - @RequestBody ObjectNode postedParams) throws IOException, ObjectNotFoundException { + protected StudentAsset copyStudentAsset(@PathVariable Integer runId, + @RequestBody ObjectNode postedParams) throws Exception { Run run = runService.retrieveById(new Long(runId)); Integer studentAssetId = postedParams.get("studentAssetId").asInt(); Integer periodId = postedParams.get("periodId").asInt(); @@ -253,27 +199,18 @@ protected String copyStudentAsset(@PathVariable Integer runId, String fileName = copiedFileName; String filePath = "/" + referencedDirName + "/" + copiedFileName; Long fileSize = studentAsset.getFileSize(); - String nodeId = null; + String nodeId = null; String componentId = null; String componentType = null; String clientDeleteTime = null; - try { - StudentAsset copiedStudentAsset = vleService.saveStudentAsset(id, runId, periodId, workgroupId, - nodeId, componentId, componentType, isReferenced, fileName, filePath, fileSize, - clientSaveTime, clientDeleteTime); - return copiedStudentAsset.toJSON().toString(); - } catch (ObjectNotFoundException e) { - e.printStackTrace(); - return "error"; - } + return vleService.saveStudentAsset(id, runId, periodId, workgroupId, nodeId, componentId, + componentType, isReferenced, fileName, filePath, fileSize, clientSaveTime, + clientDeleteTime); } else { - return "error"; + throw new Exception("Error occurred"); } } - /** - * Returns size of logged-in student's unreferenced directory - */ @GetMapping("/student/asset/{runId}/size") protected void getStudentAssetsSize(@PathVariable Long runId, HttpServletResponse response) throws IOException { @@ -286,11 +223,12 @@ protected void getStudentAssetsSize(@PathVariable Long runId, HttpServletRespons } catch (ObjectNotFoundException e) { e.printStackTrace(); } - List workgroupListByRunAndUser = - workgroupService.getWorkgroupListByRunAndUser(run, user); + List workgroupListByRunAndUser = workgroupService.getWorkgroupListByRunAndUser(run, + user); Workgroup workgroup = workgroupListByRunAndUser.get(0); Long workgroupId = workgroup.getId(); - String dirName = run.getId() + "/" + workgroupId + "/unreferenced"; // looks like /studentuploads/[runId]/[workgroupId]/unreferenced + String dirName = run.getId() + "/" + workgroupId + "/unreferenced"; // looks like + // /studentuploads/[runId]/[workgroupId]/unreferenced String path = appProperties.getProperty("studentuploads_base_dir"); String result = AssetManager.getSize(path, dirName); response.getWriter().write(result); diff --git a/src/main/java/org/wise/vle/web/wise5/StudentDataController.java b/src/main/java/org/wise/vle/web/wise5/StudentDataController.java index d6c4bf9f34..6ed410fda0 100644 --- a/src/main/java/org/wise/vle/web/wise5/StudentDataController.java +++ b/src/main/java/org/wise/vle/web/wise5/StudentDataController.java @@ -37,7 +37,6 @@ import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; -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; @@ -46,13 +45,10 @@ import org.wise.portal.dao.ObjectNotFoundException; import org.wise.portal.domain.run.Run; import org.wise.portal.domain.user.User; -import org.wise.portal.domain.workgroup.Workgroup; import org.wise.portal.presentation.web.controllers.ControllerUtil; import org.wise.portal.service.run.RunService; import org.wise.portal.service.vle.wise5.VLEService; -import org.wise.portal.service.workgroup.WorkgroupService; import org.wise.portal.spring.data.redis.MessagePublisher; -import org.wise.vle.domain.achievement.Achievement; import org.wise.vle.domain.annotation.wise5.Annotation; import org.wise.vle.domain.work.Event; import org.wise.vle.domain.work.StudentWork; @@ -60,7 +56,7 @@ /** * Controller for handling GET and POST requests of WISE5 student data WISE5 student data is stored * as StudentWork, Event, Annotation, and StudentAsset domain objects - * + * * @author Hiroki Terashima */ @Controller("wise5StudentDataController") @@ -72,9 +68,6 @@ public class StudentDataController { @Autowired private RunService runService; - @Autowired - private WorkgroupService workgroupService; - @Autowired private MessagePublisher redisPublisher; @@ -157,145 +150,6 @@ public void getWISE5StudentData(HttpServletResponse response, } } - /** - * Handles GETting achievements. Checks for permission to retrieve an existing achievement. Writes - * a list of achievements to response stream. - * - * If the student is making the request, the runId and workgroupId must be specified If the - * teacher is making the request, the runId must be specified - * - * @param id - * id of the achievement - * @param runId - * id of the run - * @param workgroupId - * id of the workgroup for whom the achievement is for - * @param achievementId - * id of the achievement in project content - * @param type - * type of achievement (e.g. "completion", "milestone") - * @param response - * response stream - */ - @RequestMapping(method = RequestMethod.GET, value = "/achievement/{runId}") - public void getWISE5StudentAchievements(@PathVariable Integer runId, - @RequestParam(value = "id", required = false) Integer id, - @RequestParam(value = "workgroupId", required = false) Integer workgroupId, - @RequestParam(value = "achievementId", required = false) String achievementId, - @RequestParam(value = "type", required = false) String type, HttpServletResponse response) { - User user = ControllerUtil.getSignedInUser(); - Run run = null; - Workgroup workgroup = null; - try { - run = runService.retrieveById(new Long(runId)); - if (workgroupId != null) { - workgroup = workgroupService.retrieveById(new Long(workgroupId)); - } - } catch (ObjectNotFoundException e) { - e.printStackTrace(); - return; - } - if (!isAssociatedWithRun(run, user, workgroup)) { - return; - } - - List achievements = vleService.getAchievements(id, runId, workgroupId, - achievementId, type); - JSONArray achievementsJSONArray = new JSONArray(); - for (int c = 0; c < achievements.size(); c++) { - Achievement achievement = achievements.get(c); - achievementsJSONArray.put(achievement.toJSON()); - } - try { - PrintWriter writer = response.getWriter(); - writer.write(achievementsJSONArray.toString()); - writer.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private Boolean isAssociatedWithRun(Run run, User user, Workgroup workgroup) { - return isStudentAssociatedWithRun(run, user, workgroup) - || isTeacherAssociatedWithRun(run, user); - } - - private Boolean isStudentAssociatedWithRun(Run run, User user, Workgroup workgroup) { - return user.isStudent() && run.isStudentAssociatedToThisRun(user) - && workgroupService.isUserInWorkgroupForRun(user, run, workgroup); - } - - private Boolean isTeacherAssociatedWithRun(Run run, User user) { - return (user.isTeacher() && run.isTeacherAssociatedToThisRun(user)) || user.isAdmin(); - } - - /** - * Handles POSTed achievements. Checks for permission and saves a new achievement or update an - * existing achievement. Writes achievement to response stream. - * - * If the student is making the request, the runId and workgroupId must be specified If the - * teacher is making the request, the runId must be specified - * - * @param id - * @param runId - * @param workgroupId - * @param achievementId - * @param type - * @param response - */ - @RequestMapping(method = RequestMethod.POST, value = "/achievement/{runId}") - public void saveWISE5StudentAchievement(@PathVariable Integer runId, - @RequestParam(value = "id", required = false) Integer id, - @RequestParam(value = "workgroupId", required = true) Integer workgroupId, - @RequestParam(value = "achievementId", required = true) String achievementId, - @RequestParam(value = "type", required = true) String type, - @RequestParam(value = "data", required = true) String data, HttpServletResponse response) - throws JSONException { - User user = ControllerUtil.getSignedInUser(); - Run run = null; - Workgroup workgroup = null; - try { - run = runService.retrieveById(new Long(runId)); - if (workgroupId != null) { - workgroup = workgroupService.retrieveById(new Long(workgroupId)); - } - } catch (ObjectNotFoundException e) { - e.printStackTrace(); - return; - } - boolean isAllowed = false; - if (user.isStudent() && run.isStudentAssociatedToThisRun(user) - && workgroupService.isUserInWorkgroupForRun(user, run, workgroup)) { - isAllowed = true; - } else if (user.isTeacher() - && (run.getOwner().equals(user) || run.getSharedowners().contains(user))) { - isAllowed = true; - } - if (!isAllowed) { - return; - } - - Achievement achievement = vleService.saveAchievement(id, runId, workgroupId, achievementId, - type, data); - try { - PrintWriter writer = response.getWriter(); - writer.write(achievement.toJSON().toString()); - writer.close(); - } catch (IOException e) { - e.printStackTrace(); - } - achievement.convertToClientAchievement(); - broadcastAchievementToTeacher(achievement); - } - - public void broadcastAchievementToTeacher(Achievement achievement) throws JSONException { - JSONObject message = new JSONObject(); - message.put("type", "achievementToTeacher"); - message.put("topic", String.format("/topic/teacher/%s", achievement.getRunId())); - message.put("achievement", achievement.toJSON()); - redisPublisher.publish(message.toString()); - } - public void broadcastAnnotationToTeacher(Annotation annotation) throws JSONException { JSONObject message = new JSONObject(); message.put("type", "annotationToTeacher"); @@ -323,7 +177,7 @@ public void broadcastStudentWorkToTeacher(StudentWork componentState) throws JSO /** * Handles batch POSTing student data (StudentWork, Action, Annotation) - * + * * @param runId * Run that the POSTer (student) is in * @param studentWorkList diff --git a/src/main/resources/version.txt b/src/main/resources/version.txt index e4df8b80ba..9c64cd9b85 100644 --- a/src/main/resources/version.txt +++ b/src/main/resources/version.txt @@ -1 +1 @@ -5.18.4 +5.19.0 diff --git a/src/main/webapp/site/src/app/app.component.html b/src/main/webapp/site/src/app/app.component.html index d467b6c5ee..691de64314 100644 --- a/src/main/webapp/site/src/app/app.component.html +++ b/src/main/webapp/site/src/app/app.component.html @@ -1,42 +1,33 @@ - - - - - - - - - - - - - - - - - - - - - arrow_upward - Back to Top - - - - - - - + + + + + + + + + + + + + + + arrow_upward + Back to Top + + + + diff --git a/src/main/webapp/site/src/app/domain/config.ts b/src/main/webapp/site/src/app/domain/config.ts index d22e021b6e..a1368f4200 100644 --- a/src/main/webapp/site/src/app/domain/config.ts +++ b/src/main/webapp/site/src/app/domain/config.ts @@ -8,4 +8,5 @@ export class Config { currentTime: number; wiseHostname?: string; wise4Hostname?: string; + discourseURL?: string; } diff --git a/src/main/webapp/site/src/app/services/config.service.ts b/src/main/webapp/site/src/app/services/config.service.ts index dee63db684..1a1f82719e 100644 --- a/src/main/webapp/site/src/app/services/config.service.ts +++ b/src/main/webapp/site/src/app/services/config.service.ts @@ -32,6 +32,10 @@ export class ConfigService { return this.config$.getValue().contextPath; } + getDiscourseURL() { + return this.config$.getValue().discourseURL; + } + getGoogleAnalyticsId() { return this.config$.getValue().googleAnalyticsId; } diff --git a/src/main/webapp/site/src/app/services/studentAssetService.spec.ts b/src/main/webapp/site/src/app/services/studentAssetService.spec.ts index 275c5b9156..1c1224553d 100644 --- a/src/main/webapp/site/src/app/services/studentAssetService.spec.ts +++ b/src/main/webapp/site/src/app/services/studentAssetService.spec.ts @@ -1,4 +1,4 @@ -import { TestBed } from '@angular/core/testing'; +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { UpgradeModule } from '@angular/upgrade/static'; import { StudentAssetService } from '../../../../wise5/services/studentAssetService'; @@ -63,18 +63,23 @@ function retrieveAssets_StudentMode_FetchAssetsAndSetAttributes() { expect(response.length).toEqual(1); expect(response[0].type).toEqual('image'); }); - http.expectOne(`${studentAssetURL}?workgroupId=${workgroupId}`).flush([asset1]); + http.expectOne(`${studentAssetURL}/${workgroupId}`).flush([asset1]); }); } function deleteAsset_StudentMode_DeleteAsset() { - it('should delete', () => { + it('should delete', fakeAsync(() => { 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(); - }); + const request = http.expectOne((req): boolean => { + return req.url.startsWith(`${studentAssetURL}/delete`); + }); + expect(request.request.method).toEqual('DELETE'); + expect(request.request.params.get('studentAssetId')).toEqual(2 as any); + expect(request.request.params.get('clientDeleteTime')).toBeDefined(); + request.flush({}); + tick(); + expect(service.allAssets.length).toEqual(0); + })); } diff --git a/src/main/webapp/site/src/app/teacher/discourse-recent-activity/discourse-recent-activity.component.html b/src/main/webapp/site/src/app/teacher/discourse-recent-activity/discourse-recent-activity.component.html new file mode 100644 index 0000000000..80dea5936f --- /dev/null +++ b/src/main/webapp/site/src/app/teacher/discourse-recent-activity/discourse-recent-activity.component.html @@ -0,0 +1,22 @@ + + + + groups + Latest from the WISE Community + + + launch + + + + + + {{topic.title}} + + + ({{topic.last_posted_at | date:'MMM dd y'}}) + + + + diff --git a/src/main/webapp/site/src/app/teacher/discourse-recent-activity/discourse-recent-activity.component.scss b/src/main/webapp/site/src/app/teacher/discourse-recent-activity/discourse-recent-activity.component.scss new file mode 100644 index 0000000000..e9ce0d3cac --- /dev/null +++ b/src/main/webapp/site/src/app/teacher/discourse-recent-activity/discourse-recent-activity.component.scss @@ -0,0 +1,37 @@ +@import '~style/abstracts/functions', '~style/abstracts/variables', '~style/abstracts/mixins'; + +ul { + list-style-type: none; + padding: 0; + margin-bottom: 0; +} + +li { + &:not(:last-of-type) { + margin-bottom: 4px; + } + + a { + max-width: 660px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + display: inline-block; + vertical-align: middle; + + @media (min-width: breakpoint('md.min')) { + max-width: 360px; + } + } +} + +.content-block { + h3 { + margin-bottom: 0; + } +} + +.content-block__title { + padding-bottom: 8px; + margin-top: -8px; +} diff --git a/src/main/webapp/site/src/app/teacher/discourse-recent-activity/discourse-recent-activity.component.spec.ts b/src/main/webapp/site/src/app/teacher/discourse-recent-activity/discourse-recent-activity.component.spec.ts new file mode 100644 index 0000000000..de44617ab4 --- /dev/null +++ b/src/main/webapp/site/src/app/teacher/discourse-recent-activity/discourse-recent-activity.component.spec.ts @@ -0,0 +1,42 @@ +import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; +import { TestBed } from "@angular/core/testing"; +import { ConfigService } from "../../services/config.service"; +import { DiscourseRecentActivityComponent } from "./discourse-recent-activity.component"; + +describe('DiscourseRecentActivityComponent', () => { + let component: DiscourseRecentActivityComponent; + let configService: ConfigService; + let http: HttpTestingController; + const discourseURL = 'http://localhost:9292'; + const sampleLatestResponse = { + users: [], + topic_list: { + topics: [{id:1},{id:2},{id:3},{id:4},{id:5}] + } + }; + + class MockConfigService { + getDiscourseURL() { + return discourseURL; + } + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ HttpClientTestingModule ], + providers: [ + DiscourseRecentActivityComponent, + { provide: ConfigService, useClass: MockConfigService } + ] + }); + component = TestBed.inject(DiscourseRecentActivityComponent); + configService = TestBed.inject(ConfigService); + http = TestBed.inject(HttpTestingController); + }); + + it('should create and show 3 latest topics', () => { + component.ngOnInit(); + http.expectOne(`${discourseURL}/latest.json?order=activity`).flush(sampleLatestResponse); + expect(component.topics.length).toEqual(3); + }); +}); diff --git a/src/main/webapp/site/src/app/teacher/discourse-recent-activity/discourse-recent-activity.component.ts b/src/main/webapp/site/src/app/teacher/discourse-recent-activity/discourse-recent-activity.component.ts new file mode 100644 index 0000000000..d44a10aa67 --- /dev/null +++ b/src/main/webapp/site/src/app/teacher/discourse-recent-activity/discourse-recent-activity.component.ts @@ -0,0 +1,27 @@ +import { HttpClient } from "@angular/common/http"; +import { Component } from "@angular/core"; +import { ConfigService } from "../../services/config.service"; + +@Component({ + selector: 'discourse-recent-activity', + templateUrl: 'discourse-recent-activity.component.html', + styleUrls: ['discourse-recent-activity.component.scss'] +}) +export class DiscourseRecentActivityComponent { + + discourseURL: string; + topics: any; + users: any; + + constructor(private configService: ConfigService, private http: HttpClient) { + } + + ngOnInit() { + this.discourseURL = this.configService.getDiscourseURL(); + this.http.get(`${this.discourseURL}/latest.json?order=activity`) + .subscribe(({topic_list, users}: any)=> { + this.topics = topic_list.topics.slice(0,3); + this.users = users; + }); + } +} diff --git a/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.html b/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.html index d85b125f61..7ff9553229 100644 --- a/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.html +++ b/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.html @@ -1,4 +1,5 @@ - + homeTeacher Home + diff --git a/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.scss b/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.scss index 36ceaefad6..1ed496494b 100644 --- a/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.scss +++ b/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.scss @@ -3,14 +3,19 @@ '~style/abstracts/variables', '~style/abstracts/mixins'; -h1 { - margin-bottom: 0; - - @media (max-width: breakpoint('xs.max')) { - font-size: mat-font-size($config, title); - } -} - .content-block { margin-top: 24px; } + +discourse-recent-activity { + margin-bottom: 16px; + margin-top: 8px; + + @media (min-width: breakpoint('sm.min')) { + margin-top: -8px; + } + + @media (min-width: breakpoint('md.min')) { + margin-top: -36px; + } +} \ No newline at end of file diff --git a/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.spec.ts b/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.spec.ts index 7980d7beb2..15977bf2da 100644 --- a/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.spec.ts +++ b/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.spec.ts @@ -92,6 +92,10 @@ export class MockConfigService { getCurrentServerTime(): number { return new Date('2018-10-17 00:00:00.0').getTime(); } + + getDiscourseURL(): string { + return 'http://localhost:9292'; + } } export class MockLibraryService { @@ -125,5 +129,6 @@ describe('TeacherHomeComponent', () => { it('should create', () => { expect(component).toBeTruthy(); + expect(component.isDiscourseEnabled).toBeTruthy(); }); }); diff --git a/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.ts b/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.ts index 2e19928507..c16383e207 100644 --- a/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.ts +++ b/src/main/webapp/site/src/app/teacher/teacher-home/teacher-home.component.ts @@ -16,10 +16,11 @@ export class TeacherHomeComponent implements OnInit { user: User = new User(); authoringToolLink: string = ''; + isDiscourseEnabled: boolean; tabLinks: any[] = [ { path: 'schedule', label: $localize`Class Schedule` }, { path: 'library', label: $localize`Unit Library` } - ] + ]; constructor(private userService: UserService, private configService: ConfigService, @@ -32,6 +33,7 @@ export class TeacherHomeComponent implements OnInit { this.configService.getConfig().subscribe((config) => { if (config != null) { this.authoringToolLink = `${this.configService.getContextPath()}/teacher/edit/home`; + this.isDiscourseEnabled = this.configService.getDiscourseURL() != null; } }); } diff --git a/src/main/webapp/site/src/app/teacher/teacher.module.ts b/src/main/webapp/site/src/app/teacher/teacher.module.ts index cf56de3c28..cbe4353fe9 100644 --- a/src/main/webapp/site/src/app/teacher/teacher.module.ts +++ b/src/main/webapp/site/src/app/teacher/teacher.module.ts @@ -38,6 +38,7 @@ import { RunSettingsDialogComponent } from './run-settings-dialog/run-settings-d import { UseWithClassWarningDialogComponent } from './use-with-class-warning-dialog/use-with-class-warning-dialog.component'; import { EditRunWarningDialogComponent } from './edit-run-warning-dialog/edit-run-warning-dialog.component'; import { ListClassroomCoursesDialogComponent } from './list-classroom-courses-dialog/list-classroom-courses-dialog.component'; +import { DiscourseRecentActivityComponent } from './discourse-recent-activity/discourse-recent-activity.component'; import { ShareRunCodeDialogComponent } from './share-run-code-dialog/share-run-code-dialog.component'; const materialModules = [ @@ -61,6 +62,7 @@ const materialModules = [ ], declarations: [ CreateRunDialogComponent, + DiscourseRecentActivityComponent, TeacherComponent, TeacherHomeComponent, TeacherRunListComponent, diff --git a/src/main/webapp/site/src/messages.xlf b/src/main/webapp/site/src/messages.xlf index f725d63e46..00a5ed0f82 100644 --- a/src/main/webapp/site/src/messages.xlf +++ b/src/main/webapp/site/src/messages.xlf @@ -501,7 +501,7 @@ Back to Top app/app.component.html - 35 + 29 @@ -5353,11 +5353,18 @@ 12 + + Latest from the WISE Community + + app/teacher/discourse-recent-activity/discourse-recent-activity.component.html + 5 + + Teacher Home app/teacher/teacher-home/teacher-home.component.html - 7 + 8 app/modules/header/header-account-menu/header-account-menu.component.html @@ -5368,14 +5375,14 @@ Authoring Tool app/teacher/teacher-home/teacher-home.component.html - 20 + 23 Teacher home navigation app/teacher/teacher-home/teacher-home.component.html - 11 + 14 diff --git a/src/main/webapp/site/src/style/layout/_standalone.scss b/src/main/webapp/site/src/style/layout/_standalone.scss index f9248df3d3..7d6d38baa7 100644 --- a/src/main/webapp/site/src/style/layout/_standalone.scss +++ b/src/main/webapp/site/src/style/layout/_standalone.scss @@ -1,5 +1,5 @@ .standalone { - padding: 32px; + height: 100%; .mat-card-content { margin-bottom: 16px; @@ -25,6 +25,7 @@ .standalone__logo { margin: 0 auto 16px; + padding-top: 32px; max-width: 300px; img { @@ -35,7 +36,7 @@ .standalone__content { max-width: breakpoint('sm.min'); - margin: 0 auto 16px; + margin: 0 auto 32px; @media (min-width: breakpoint('sm.min')) { &.mat-card { diff --git a/src/main/webapp/wise5/services/studentAssetService.ts b/src/main/webapp/wise5/services/studentAssetService.ts index 7c1ab64165..09429e1899 100644 --- a/src/main/webapp/wise5/services/studentAssetService.ts +++ b/src/main/webapp/wise5/services/studentAssetService.ts @@ -34,14 +34,11 @@ export class StudentAssetService { 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) + return this.http.get( + `${this.ConfigService.getStudentAssetsURL()}/${this.ConfigService.getWorkgroupId()}`) .toPromise().then((assets: any) => { - // loop through the assets and make them into JSON object with more details - let result = []; - let studentUploadsBaseURL = this.ConfigService.getStudentUploadsBaseURL(); + this.allAssets = []; + const studentUploadsBaseURL = this.ConfigService.getStudentUploadsBaseURL(); for (const asset of assets) { if (!asset.isReferenced && asset.serverDeleteTime == null && asset.fileName !== '.DS_Store') { @@ -56,11 +53,10 @@ export class StudentAssetService { asset.type = 'file'; asset.iconURL = 'wise5/vle/notebook/file.png'; } - result.push(asset); + this.allAssets.push(asset); } } - this.allAssets = result; - return result; + return this.allAssets; }); } } @@ -212,21 +208,22 @@ export class StudentAssetService { } } - deleteAsset(studentAsset) { + deleteAsset(studentAsset: any) { 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); + let httpParams = new HttpParams(); + httpParams = httpParams.set('studentAssetId', studentAsset.id); + httpParams = httpParams.set('workgroupId', this.ConfigService.getWorkgroupId()); + httpParams = httpParams.set('periodId', this.ConfigService.getPeriodId()); + httpParams = httpParams.set('clientDeleteTime', `${Date.parse(new Date().toString())}`); + const options = { params: httpParams }; + return this.http.delete(`${this.ConfigService.getStudentAssetsURL()}/delete`, options) + .toPromise().then(() => { + this.allAssets.splice(this.allAssets.indexOf(studentAsset), 1); return studentAsset; }); } diff --git a/src/test/java/org/wise/portal/domain/studentAsset/StudentAssetTest.java b/src/test/java/org/wise/portal/domain/studentAsset/StudentAssetTest.java new file mode 100644 index 0000000000..bdf29e3a9a --- /dev/null +++ b/src/test/java/org/wise/portal/domain/studentAsset/StudentAssetTest.java @@ -0,0 +1,58 @@ +package org.wise.portal.domain.studentAsset; + +import static org.junit.Assert.assertEquals; + +import java.sql.Timestamp; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.easymock.EasyMockRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.wise.portal.domain.DomainTest; +import org.wise.portal.service.work.StudentAssetJsonModule; +import org.wise.vle.domain.work.StudentAsset; +import org.wise.vle.domain.work.StudentAssetSerializer; + +@RunWith(EasyMockRunner.class) +public class StudentAssetTest extends DomainTest { + + StudentAsset asset; + + ObjectMapper mapper; + + StudentAssetJsonModule jsonModule = new StudentAssetJsonModule(); + + @Before + public void setup() { + super.setup(); + jsonModule.addSerializer(StudentAsset.class, new StudentAssetSerializer()); + mapper = new ObjectMapper(); + mapper.registerModule(jsonModule); + asset = new StudentAsset(); + asset.setId(15); + asset.setRun(run); + asset.setPeriod(period); + asset.setWorkgroup(workgroup); + asset.setIsReferenced(false); + asset.setFileName("abc.png"); + asset.setFilePath("/345/assets"); + asset.setFileSize(512L); + asset.setClientSaveTime(new Timestamp(1L)); + asset.setServerSaveTime(new Timestamp(2L)); + asset.setClientDeleteTime(new Timestamp(5L)); + asset.setServerDeleteTime(new Timestamp(6L)); + } + + @Test + public void serialize() throws Exception { + String json = mapper.writeValueAsString(asset); + assertEquals("{\"id\":15,\"runId\":1,\"periodId\":100,\"workgroupId\":64,\"nodeId\":null," + + "\"componentId\":null,\"componentType\":null,\"isReferenced\":false," + + "\"fileName\":\"abc.png\",\"filePath\":\"/345/assets\",\"fileSize\":512," + + "\"clientSaveTime\":1,\"serverSaveTime\":2,\"clientDeleteTime\":5,\"serverDeleteTime\":6}", + json); + } + +} diff --git a/src/test/java/org/wise/portal/domain/work/AchievementTest.java b/src/test/java/org/wise/portal/domain/work/AchievementTest.java new file mode 100644 index 0000000000..eee7864213 --- /dev/null +++ b/src/test/java/org/wise/portal/domain/work/AchievementTest.java @@ -0,0 +1,50 @@ +package org.wise.portal.domain.work; + +import static org.junit.Assert.assertEquals; + +import java.sql.Timestamp; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.easymock.EasyMockRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.wise.portal.domain.DomainTest; +import org.wise.portal.service.work.AchievementJsonModule; +import org.wise.vle.domain.achievement.Achievement; +import org.wise.vle.domain.achievement.AchievementSerializer; + +@RunWith(EasyMockRunner.class) +public class AchievementTest extends DomainTest { + + Achievement achievement; + + ObjectMapper mapper; + + AchievementJsonModule jsonModule = new AchievementJsonModule(); + + @Before + public void setup() { + super.setup(); + jsonModule.addSerializer(Achievement.class, new AchievementSerializer()); + mapper = new ObjectMapper(); + mapper.registerModule(jsonModule); + achievement = new Achievement(); + achievement.setId(12); + achievement.setRun(run); + achievement.setWorkgroup(workgroup); + achievement.setAchievementId("achievement_1"); + achievement.setType("milestoneReport"); + achievement.setData("{}"); + achievement.setAchievementTime(new Timestamp(1L)); + } + + @Test + public void serialize() throws JsonProcessingException { + String json = mapper.writeValueAsString(achievement); + assertEquals("{\"id\":12,\"runId\":1,\"workgroupId\":64,\"achievementId\":\"achievement_1\"," + + "\"type\":\"milestoneReport\",\"achievementTime\":1,\"data\":{}}", json); + } +} diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/DiscourseSSOControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/DiscourseSSOControllerTest.java new file mode 100644 index 0000000000..ea19501a86 --- /dev/null +++ b/src/test/java/org/wise/portal/presentation/web/controllers/DiscourseSSOControllerTest.java @@ -0,0 +1,40 @@ +package org.wise.portal.presentation.web.controllers; + +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; + +import org.easymock.EasyMockRunner; +import org.easymock.TestSubject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.web.servlet.view.RedirectView; + +@RunWith(EasyMockRunner.class) +public class DiscourseSSOControllerTest extends APIControllerTest { + + @TestSubject + private DiscourseSSOController discourseSSOController = new DiscourseSSOController(); + + String base64EncodedSSO = "bm9uY2U9MWJmMDQwNzIzYmYwNDc2NzExZjAxMWY4MjYyNzQyMTQmcmV0dXJuX3Nzb191" + + "cmw9aHR0cCUzQSUyRiUyRmxvY2FsaG9zdCUzQTkyOTIlMkZzZXNzaW9uJTJGc3NvX2xvZ2lu"; + String sigParam = "13f83c3dc28af7c37fac8d40f7792d63bf727cc2e7b293f3669a526dd861f71d"; + String redirectURL = "http://localhost:9292/session/sso_login?sso=bm9uY2U9MWJmMDQwNzIzYmYwNDc2N" + + "zExZjAxMWY4MjYyNzQyMTQmcmV0dXJuX3Nzb191cmw9aHR0cCUzQSUyRiUyRmxvY2FsaG9zdCUzQTkyOTIlMkZzZXN" + + "zaW9uJTJGc3NvX2xvZ2luJm5hbWU9U3F1aWR3YXJkK1RlbnRhY2xlcyZ1c2VybmFtZT1TcXVpZHdhcmRUZW50YWNsZ" + + "XMmZW1haWw9JmV4dGVybmFsX2lkPTk0MjEw&sig=9e6d86e5ac58afe16acf62fe7aa11aec4ef3540a8eb7c0d56a" + + "a6c585b90bee61"; + + @Test + public void discourseSSOLogin_ValidArgs_ReturnRedirectURL() throws Exception { + expect(userService.retrieveUserByUsername(teacherAuth.getName())).andReturn(teacher1); + expect(appProperties.getProperty("discourse_sso_secret_key")).andReturn("do_the_right_thing"); + expect(appProperties.getProperty("discourse_url")).andReturn("http://localhost:9292"); + replay(userService, appProperties); + RedirectView discourseSSOLoginRedirect = + discourseSSOController.discourseSSOLogin(base64EncodedSSO, sigParam, teacherAuth); + assertEquals(redirectURL, discourseSSOLoginRedirect.getUrl()); + verify(userService, appProperties); + } +} diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/user/UserAPIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/user/UserAPIControllerTest.java index 11c392e136..251fcb0b47 100644 --- a/src/test/java/org/wise/portal/presentation/web/controllers/user/UserAPIControllerTest.java +++ b/src/test/java/org/wise/portal/presentation/web/controllers/user/UserAPIControllerTest.java @@ -64,6 +64,7 @@ public void getConfig_WISEContextPath_ReturnConfig() { expect(appProperties.get("google_analytics_id")).andReturn("UA-XXXXXX-1"); expect(appProperties.get("recaptcha_public_key")).andReturn("recaptcha-123-abc"); expect(appProperties.get("wise4.hostname")).andReturn("http://localhost:8080/legacy"); + expect(appProperties.getOrDefault("discourse_url", null)).andReturn("http://localhost:9292"); expect(appProperties.get("wise.hostname")).andReturn("http://localhost:8080"); replay(appProperties); HashMap config = userAPIController.getConfig(request);