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 @@ - - -
-
- - - - - - - - - -
- -
-
- -
- -
-
-
-
- - + + + + + + + +
+ +
+
+ +
+ +
+
+
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 + +
+ +
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

+