From 5aa8d04cc3db1d806e50a3e4820d58976b81d9ef Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Fri, 26 Apr 2024 22:52:36 +0200 Subject: [PATCH 01/10] #1633 --- .../migrations/20_group_field_added.sql | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 modules/fbs-core/api/src/main/resources/migrations/20_group_field_added.sql diff --git a/modules/fbs-core/api/src/main/resources/migrations/20_group_field_added.sql b/modules/fbs-core/api/src/main/resources/migrations/20_group_field_added.sql new file mode 100644 index 000000000..04f8d04ce --- /dev/null +++ b/modules/fbs-core/api/src/main/resources/migrations/20_group_field_added.sql @@ -0,0 +1,21 @@ +BEGIN; + + +CREATE TABLE IF NOT EXISTS `fbs`.`group` ( + `group_id` INT NOT NULL AUTO_INCREMENT, + `course_id` INT NOT NULL, + `name` VARCHAR(100) NOT NULL, + `membership` INT NOT NULL, + `visible` TINYINT(1) NOT NULL DEFAULT 1, + PRIMARY KEY (`group_id`), + FOREIGN KEY (`course_id`) REFERENCES `fbs`.`course`(`course_id`), + UNIQUE INDEX `groups_groupid_courseid_uindex` (`group_id`, `course_id`)) + ENGINE = InnoDB + DEFAULT CHARACTER SET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; + +INSERT INTO migration (number) VALUES (20); + +COMMIT; + + From 1b0d9c9230a95af14df9108f6b8a5b73cac1cbf5 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Fri, 26 Apr 2024 23:09:34 +0200 Subject: [PATCH 02/10] Create model for groups (#1633) --- .../src/main/scala/de/thm/ii/fbs/model/Group.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Group.scala diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Group.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Group.scala new file mode 100644 index 000000000..1aa690cd5 --- /dev/null +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Group.scala @@ -0,0 +1,12 @@ +package de.thm.ii.fbs.model + +/** + * A group + * @param id The id of the group + * @param courseId course to which the group belongs + * @param name Name of the group + * @param membership The max number of members + * @param visible The visibility of the group, false = invisible + */ + +case class Group(id: Int, courseId: Int, name: String, membership: Int, visible: Boolean = true) \ No newline at end of file From ec89797a138a3ad32d9373d871e668d8bc5b430d Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Sun, 28 Apr 2024 11:34:34 +0200 Subject: [PATCH 03/10] Add group controller (#1634) --- .../ii/fbs/controller/GroupController.scala | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupController.scala diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupController.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupController.scala new file mode 100644 index 000000000..4bae1aec2 --- /dev/null +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupController.scala @@ -0,0 +1,154 @@ +package de.thm.ii.fbs.controller + +import com.fasterxml.jackson.databind.JsonNode +import de.thm.ii.fbs.controller.exception.{BadRequestException, ForbiddenException, ResourceNotFoundException} +import de.thm.ii.fbs.model.{Group, CourseRole, GlobalRole} +import de.thm.ii.fbs.services.persistence._ +import de.thm.ii.fbs.services.security.AuthService +import de.thm.ii.fbs.util.JsonWrapper._ + +import javax.servlet.http.{HttpServletRequest, HttpServletResponse} +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation._ + + +/** + * Controller to manage rest api calls for a group resource. + */ + +@RestController +@CrossOrigin +@RequestMapping (path = Array("/api/v1/courses/{cid}/groups"), produces = Array(MediaType.APPLICATION_JSON_VALUE)) +class GroupController{ + @Autowired + private val groupService : GroupService = null + @Autowired + private val authService : AuthService = null + @Autowired + private val courseRegistrationService: CourseRegistrationService = null + + /** + * Get a group list + * + * @param cid Course Id + * @param ignoreHidden optional filter to filter only for visible groups + * @param req http request + * @param res http response + * @return group list + */ + @GetMapping(value = Array("")) + @ResponseBody + def getAll(@PathVariable ("cid") cid : Integer, @RequestParam(value = "visible", required = false) ignoreHidden: Boolean,req: HttpServletRequest, res: HttpServletResponse): List[Group] = { + val user = authService.authorize(req, res) + val someCourseRole = courseRegistrationService.getParticipants(cid).find(_.user.id == user.id).map(_.role) + (user.globalRole, someCourseRole) match { + case (GlobalRole.ADMIN | GlobalRole.MODERATOR, _) | (_, Some(CourseRole.DOCENT)) => + val groupList = groupService.getAll(cid, false) + groupList + case _ => throw new ForbiddenException() + } + } + + /** + * Create a new group + * + * @param cid Course Id + * @param req http request + * @param res http response + * @param body contains JSON request + * @return JSON + */ + @PostMapping(value = Array(""), consumes = Array(MediaType.APPLICATION_JSON_VALUE)) + @ResponseBody + def create(@PathVariable ("cid") cid : Int, req: HttpServletRequest, res: HttpServletResponse, @RequestBody body: JsonNode): Group = { + val user = authService.authorize(req, res) + val someCourseRole = courseRegistrationService.getParticipants(cid).find(_.user.id == user.id).map(_.role) + if (!(user.globalRole == GlobalRole.ADMIN || user.globalRole == GlobalRole.MODERATOR || someCourseRole == Some(CourseRole.DOCENT))) { + throw new ForbiddenException() + } + val name = Option(body.get("name")).map(_.asText()) + val membership = Option(body.get("membership")).map(_.asInt()) + val visible = Option(body.get("visible")).map(_.asBoolean()) + (name, membership, visible) match { + case (Some(name), Some (membership), Some(visible)) + => groupService.create(Group(0, cid, name, membership, visible)) + case _ => throw new BadRequestException("Malformed Request Body") + } + } + + /** + * Get a single group by id + * + * @param cid Course Id + * @param gid Group id + * @param req http request + * @param res http response + * @return A single group + */ + @GetMapping(value = Array("/{gid}")) + @ResponseBody + def getOne(@PathVariable ("cid") cid : Integer, @PathVariable("gid") gid: Integer, req: HttpServletRequest, res: HttpServletResponse): Group = { + val user = authService.authorize(req, res) + val someCourseRole = courseRegistrationService.getParticipants(cid).find(_.user.id == user.id).map(_.role) + + groupService.get(cid, gid) match { + case Some(group) => if (!(user.globalRole == GlobalRole.ADMIN || user.globalRole == GlobalRole.MODERATOR || someCourseRole == Some(CourseRole.DOCENT))) { + throw new ForbiddenException() + } else { + group + } + case _ => throw new ResourceNotFoundException() + } + } + + /** + * Update a single group by id + * + * @param cid Course id + * @param gid Group id + * @param req http request + * @param res http response + * @param body Request Body + */ + @PutMapping(value = Array("/{gid}")) + def update(@PathVariable ("cid") cid : Integer, @PathVariable("gid") gid: Integer, req: HttpServletRequest, res: HttpServletResponse, + @RequestBody body: JsonNode): Unit = { + val user = authService.authorize(req, res) + val someCourseRole = courseRegistrationService.getParticipants(cid).find(_.user.id == user.id).map(_.role) + + (user.globalRole, someCourseRole) match { + case (GlobalRole.ADMIN | GlobalRole.MODERATOR, _) | (_, Some(CourseRole.DOCENT)) => + (body.retrive("name").asText(), + body.retrive("membership").asInt(), + body.retrive("visible").asBool() + ) match { + case (Some(name), Some (membership), visible) + => groupService.update(cid, gid, Group(gid, cid, name, membership, visible.getOrElse(true))) + case _ => throw new BadRequestException("Malformed Request Body") + } + case _ => throw new ForbiddenException() + } + } + + /** + * Delete course + * + * @param cid Course id + * @param gid Group id + * @param req http request + * @param res http response + */ + @DeleteMapping(value = Array("/{gid}")) + def delete(@PathVariable ("cid") cid : Integer, @PathVariable("gid") gid: Integer, req: HttpServletRequest, res: HttpServletResponse): Unit = { + val user = authService.authorize(req, res) + val someCourseRole = courseRegistrationService.getParticipants(cid).find(_.user.id == user.id).map(_.role) + + (user.globalRole, someCourseRole) match { + case (GlobalRole.ADMIN | GlobalRole.MODERATOR, _) | (_, Some(CourseRole.DOCENT)) => + groupService.delete(cid, gid) + case _ => throw new ForbiddenException() + } + } +} \ No newline at end of file From c75c59f2dbca43a6a1b17616583b8ee649769276 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Sun, 28 Apr 2024 11:47:37 +0200 Subject: [PATCH 04/10] Add group service (#1634) --- .../services/persistence/GroupService.scala | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupService.scala diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupService.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupService.scala new file mode 100644 index 000000000..87ce67f4d --- /dev/null +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupService.scala @@ -0,0 +1,89 @@ +package de.thm.ii.fbs.services.persistence + +import java.math.BigInteger +import java.sql.{ResultSet, SQLException} + +import de.thm.ii.fbs.model.Group +import de.thm.ii.fbs.util._ +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.stereotype.Component + +/** + * Handles the creation, deletion and modifications of groups persistant state. + */ +@Component +class GroupService{ + @Autowired + private implicit val jdbc: JdbcTemplate = null + + /** + * Get a group list + * + * @param cid Course id + * @param ignoreHidden If true only visible groups will be returned + * @return List of groups + */ + def getAll(cid: Int, ignoreHidden: Boolean = true): List[Group] = DB.query( + s"SELECT group_id, course_id, name, membership, visible FROM `group` WHERE" + (if (ignoreHidden) " visible = 1 AND" else "") + s" course_id = $cid", + (res, _) => parseResult(res) + ) + + /** + * Create a new group + * + * @param group The group + * @return The created group with id + */ + def create(group: Group): Group = { + DB.insert("INSERT INTO `group` (course_id, name, membership, visible) VALUES (?, ?, ?, ?);", group.courseId, group.name, group.membership, group.visible) + .map(gk => gk(0).asInstanceOf[BigInteger].intValue()) + .flatMap(id => get(group.courseId, id)) match { + case Some(group) => group + case None => throw new SQLException("Group could not be created") + } + } + + + /** + * Get a single group by id + * + * @param cid Course id + * @param gid Group id + * @return The found Group + */ + def get(cid: Int, gid: Int): Option[Group] = DB.query( + "SELECT group_id, course_id, name, membership, visible FROM `group` WHERE course_id = ? AND group_id = ?", + (res, _) => parseResult(res), cid, gid).headOption + + + /** + * Update a single group by id + * + * @param cid Course id + * @param gid Group id + * @param group The group + * @return True if successful + */ + def update(cid: Int, gid: Int, group: Group): Boolean = { + 1 == DB.update("UPDATE `group` SET name = ?, membership = ?, visible = ? WHERE course_id = ? AND group_id = ?", group.name, group.membership, group.visible,cid, gid) + } + + /** + * Delete a single group by id + * + * param cid Course id + * @param gid Group id + * @return True if successful + */ + def delete(cid: Int, gid: Int): Boolean = 1 == DB.update("DELETE FROM `group` WHERE course_id = ? AND group_id = ?", cid, gid) + + + private def parseResult(res: ResultSet): Group = Group( + id = res.getInt("group_id"), + courseId = res.getInt("course_id"), + name = res.getString("name"), + membership = res.getInt("membership"), + visible = res.getBoolean("visible"), + ) +} From f2c64fe6a81f4376f96d7d724c898c3382e98dae Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Tue, 30 Apr 2024 22:42:07 +0200 Subject: [PATCH 05/10] Add migration for user group field (#1635) --- .../migrations/21_user_group_field_added.sql | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 modules/fbs-core/api/src/main/resources/migrations/21_user_group_field_added.sql diff --git a/modules/fbs-core/api/src/main/resources/migrations/21_user_group_field_added.sql b/modules/fbs-core/api/src/main/resources/migrations/21_user_group_field_added.sql new file mode 100644 index 000000000..22a786f3e --- /dev/null +++ b/modules/fbs-core/api/src/main/resources/migrations/21_user_group_field_added.sql @@ -0,0 +1,51 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS `fbs`.`user_group` ( + `user_id` INT NOT NULL, + `course_id` INT NOT NULL, + `group_id` INT NOT NULL, + PRIMARY KEY (`user_id`, `course_id`, `group_id`), + INDEX `user_has_groups_users_user_id_fk` (`user_id` ASC), + CONSTRAINT `user_has_groups_users_user_id_fk` + FOREIGN KEY (`user_id`) + REFERENCES `fbs`.`user` (`user_id`) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT `user_group_course_course_id_fk` + FOREIGN KEY (`course_id`) + REFERENCES `fbs`.`course` (`course_id`) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT `user_group_group_group_id_fk` + FOREIGN KEY (`group_id`) + REFERENCES `fbs`.`group` (`group_id`) + ON DELETE CASCADE + ON UPDATE CASCADE +) ENGINE = InnoDB + DEFAULT CHARACTER SET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; + +-- ----------------------------------------------------- +-- trigger to check if user is participant of the course +-- ----------------------------------------------------- + +CREATE TRIGGER check_user_course_before_insert +BEFORE INSERT ON `user_group` +FOR EACH ROW +BEGIN + DECLARE user_in_course INT; + SELECT COUNT(*) + INTO user_in_course + FROM user_course + WHERE user_id = NEW.user_id AND course_id = NEW.course_id; + + IF user_in_course = 0 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Der Nutzer gehört nicht zum angegebenen Kurs.'; + END IF; +END; + + +INSERT INTO migration (number) VALUES (21); + +COMMIT; + From f4b9cf682f8e9ab82fadfead760264cd4979dc60 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Tue, 30 Apr 2024 22:51:06 +0200 Subject: [PATCH 06/10] Add Controller for Group Registration (#1636) --- .../GroupRegistrationController.scala | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupRegistrationController.scala diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupRegistrationController.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupRegistrationController.scala new file mode 100644 index 000000000..b86bdefe4 --- /dev/null +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupRegistrationController.scala @@ -0,0 +1,153 @@ +package de.thm.ii.fbs.controller + +import com.fasterxml.jackson.databind.JsonNode +import de.thm.ii.fbs.controller.exception.{BadRequestException, ForbiddenException, ResourceNotFoundException, MembershipExceededException} +import de.thm.ii.fbs.model.{Group, CourseRole, GlobalRole, User, Participant} +import de.thm.ii.fbs.services.persistence._ +import de.thm.ii.fbs.services.security.AuthService +import de.thm.ii.fbs.util.JsonWrapper._ + +import javax.servlet.http.{HttpServletRequest, HttpServletResponse} +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation._ + +/** + * Controller to manage rest api calls for group registration and group members. + */ + +@RestController +@CrossOrigin +@RequestMapping(path = Array("/api/v1"), produces = Array(MediaType.APPLICATION_JSON_VALUE)) +class GroupRegistrationController { + @Autowired + private val authService: AuthService = null + @Autowired + private val courseRegistrationService: CourseRegistrationService = null + @Autowired + private val groupService: GroupService = null + @Autowired + private val groupRegistrationService: GroupRegistrationService = null + + /** + * Add a user to a group within a course + * + * @param cid Course id + * @param gid Group id + * @param uid User id + * @param req http request + * @param res http response + * @param body Content + */ + @PutMapping(value = Array("/courses/{cid}/groups/{gid}/users/{uid}"), consumes = Array(MediaType.APPLICATION_JSON_VALUE)) + def addUserToGroup(@PathVariable("cid") cid: Int, @PathVariable("gid") gid: Int, @PathVariable("uid") uid: Int, + req: HttpServletRequest, res: HttpServletResponse): Unit = { + val user = authService.authorize(req, res) + val hasGlobalPrivileges = user.hasRole(GlobalRole.ADMIN, GlobalRole.MODERATOR) + val hasCoursePrivileges = courseRegistrationService.getCoursePrivileges(user.id).getOrElse(cid, CourseRole.STUDENT) == CourseRole.DOCENT + if (hasGlobalPrivileges || hasCoursePrivileges || user.id == uid) { + //Check if the group is full + val currentMembership = groupRegistrationService.getGroupMembership(cid,gid) + val group = groupService.get(cid, gid) + group match { + case Some(group) => val maxMembership: Int = group.membership + if (currentMembership < maxMembership) { + groupRegistrationService.addUserToGroup(uid, cid, gid) + } else { + throw new MembershipExceededException() + } + case _ => throw new ResourceNotFoundException() + } + } else { + throw new ForbiddenException() + } + } + + /** + * Remove a user from a group + * + * @param uid User id + * @param cid Course id + * @param gid Group id + * @param req http request + * @param res http response + */ + @DeleteMapping(value = Array("/courses/{cid}/groups/{gid}/users/{uid}")) + def removeUserFromGroup(@PathVariable("uid") uid: Int, @PathVariable("cid") cid: Int, @PathVariable("gid") gid: Int, req: HttpServletRequest, res: HttpServletResponse): Unit = { + val user = authService.authorize(req, res) + val hasGlobalPrivileges = user.hasRole(GlobalRole.ADMIN, GlobalRole.MODERATOR) + val hasCoursePrivileges = courseRegistrationService.getCoursePrivileges(user.id).getOrElse(cid, CourseRole.STUDENT) == CourseRole.DOCENT + if (hasGlobalPrivileges || hasCoursePrivileges || user.id == uid) { + groupRegistrationService.removeUserFromGroup(uid, cid, gid) + } else { + throw new ForbiddenException() + } + } + + /** + * Remove all users from a group + * + * @param cid Course id + * @param gid Group id + * @param req http request + * @param res http response + */ + @DeleteMapping(value = Array("/courses/{cid}/groups/{gid}/users")) + def removeUserFromGroup(@PathVariable("cid") cid: Int, @PathVariable("gid") gid: Int, req: HttpServletRequest, res: HttpServletResponse): Unit = { + val user = authService.authorize(req, res) + val hasGlobalPrivileges = user.hasRole(GlobalRole.ADMIN, GlobalRole.MODERATOR) + val hasCoursePrivileges = courseRegistrationService.getCoursePrivileges(user.id).getOrElse(cid, CourseRole.STUDENT) == CourseRole.DOCENT + if (hasGlobalPrivileges || hasCoursePrivileges) { + groupRegistrationService.removeAllUsersFromGroup(cid, gid) + } else { + throw new ForbiddenException() + } + } + + /** + * Retrieve all groups of a specific user + * + * @param uid User id + * @param req http request + * @param res http response + * @return List of Groups + */ + @GetMapping(value = Array("/users/{uid}/groups")) + @ResponseBody + def getUserGroups(@PathVariable("uid") uid: Integer, req: HttpServletRequest, res: HttpServletResponse): List[Group] = { + val user = authService.authorize(req, res) + val hasGlobalPrivileges = user.hasRole(GlobalRole.ADMIN, GlobalRole.MODERATOR) + if (hasGlobalPrivileges || user.id == uid) { + groupRegistrationService.getUserGroups(uid, ignoreHidden = false) + } else { + throw new ForbiddenException() + } + } + + /** + * Get all course participants which are part of a group + * @param cid Course id + * @param gid Group id + * @param req http request + * @param res http response + * @return List of course participants + */ + @GetMapping(value = Array("/courses/{cid}/groups/{gid}/participants")) + @ResponseBody + def getMembers(@PathVariable("cid") cid: Integer, @PathVariable("gid") gid: Int, req: HttpServletRequest, res: HttpServletResponse): List[Participant] = { + val user = authService.authorize(req, res) + val hasGlobalPrivileges = user.hasRole(GlobalRole.ADMIN, GlobalRole.MODERATOR) + val hasCoursePrivileges = courseRegistrationService.getCoursePrivileges(user.id).getOrElse(cid, CourseRole.STUDENT) == CourseRole.DOCENT + if (hasGlobalPrivileges || hasCoursePrivileges) { + groupRegistrationService.getMembers(cid, gid) + } else { + throw new ForbiddenException() + } + } +} + + + + + From 426705c5d64bf2932ca5ea004dd5725d8a446b8c Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Tue, 30 Apr 2024 22:52:54 +0200 Subject: [PATCH 07/10] Add Service for Group Registration (#1636) --- .../GroupRegistrationService.scala | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupRegistrationService.scala diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupRegistrationService.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupRegistrationService.scala new file mode 100644 index 000000000..7d5d8a632 --- /dev/null +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupRegistrationService.scala @@ -0,0 +1,112 @@ +package de.thm.ii.fbs.services.persistence + +import de.thm.ii.fbs.model._ +import de.thm.ii.fbs.util.DB +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.stereotype.Component + +import java.sql.ResultSet +import org.springframework.jdbc.core.RowMapper + +/** + * Handles group registration and participants. + */ +@Component +class GroupRegistrationService { + @Autowired + private implicit val jdbc: JdbcTemplate = null + + /** + * Add a user to a group + * + * @param uid User id + * @param cid Course id + * @param gid Group id + * @return True if successful + */ + def addUserToGroup(uid:Int, cid: Int, gid: Int): Boolean = + 1 == DB.update("INSERT INTO user_group (user_id, course_id, group_id) VALUES (?,?,?);",uid, cid, gid) + + /** + * Remove a user from a group + * + * @param uid User id + * @param cid Course id + * @param gid Group id + * @return True if sucessfully deregistered + */ + def removeUserFromGroup(uid: Int, cid: Int, gid: Int): Boolean = + 1 == DB.update("DELETE FROM user_group WHERE user_id = ? AND course_id = ? AND group_id = ?",uid, cid, gid) + + /** + * Remove all users from a group + * + * @param cid Course id + * @param gid Group id + * @return True if sucessfully deregistered + */ + def removeAllUsersFromGroup(cid: Int, gid: Int): Boolean = + 1 == DB.update("DELETE FROM user_group WHERE course_id = ? AND group_id = ?", cid, gid) + + /** + * Retrieve all groups of a specific user + * + * @param uid User id + * @param ignoreHidden True if hidden groups should be ignored + * @return List of groups + */ + def getUserGroups(uid: Int, ignoreHidden: Boolean = true): List[Group] = DB.query( + "SELECT g.group_id, g.course_id, g.name, g.membership, g.visible FROM `group` g JOIN user_group ug ON g.group_id = ug.group_id WHERE ug.user_id = ? ORDER BY g.course_id ASC" + + (if (ignoreHidden) " AND g.visible = 1" else ""), + (res, _) => parseResult(res), uid) + + /** + * Get all members of a group + * + * @param cid Course id + * @param gid Group id + * @return List of members + */ + def getMembers(cid: Int, gid: Int): List[Participant] = DB.query( + "SELECT u.user_id, u.prename, u.surname, u.email, u.username, u.alias, u.global_role, uc.course_role " + + "FROM user u " + + "JOIN user_course uc ON u.user_id = uc.user_id " + + "JOIN user_group ug ON uc.course_id = ug.course_id AND uc.user_id = ug.user_id " + + "WHERE u.deleted = 0 AND ug.course_id = ? AND ug.group_id = ?", + (res, _) => Participant(parseUserResult(res), CourseRole.parse(res.getInt("course_role"))), cid, gid) + + /** + * Gets current number of members of a group + * + * @param cid Course id + * @param gid Group id + * @return Number of members + */ + def getGroupMembership(cid: Int, gid: Int) : Int = { + val groupMembershipRowMapper: RowMapper[Int] = (rs: ResultSet, rowNum: Int) => rs.getInt(1) + val sql = "SELECT COUNT(*) FROM user_group WHERE course_id = ? AND group_id = ?" + return jdbc.queryForObject(sql, groupMembershipRowMapper, cid, gid); + } + + private def parseResult(res: ResultSet): Group = Group( + id = res.getInt("group_id"), + courseId = res.getInt("course_id"), + name = res.getString("name"), + membership = res.getInt("membership"), + visible = res.getBoolean("visible"), + ) + + private def parseUserResult(res: ResultSet): User = new User( + prename = res.getString("prename"), + surname = res.getString("surname"), + email = res.getString("email"), + username = res.getString("username"), + globalRole = GlobalRole.parse(res.getInt("global_role")), + alias = Option(res.getString("alias")), + id = res.getInt("user_id") + ) +} + + + \ No newline at end of file From 7135ff47333a64763d715c2ed12638e099994bd6 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Tue, 30 Apr 2024 23:00:44 +0200 Subject: [PATCH 08/10] Add MembershipExceedException (#1637) --- .../exception/MembershipExceededException.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/exception/MembershipExceededException.scala diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/exception/MembershipExceededException.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/exception/MembershipExceededException.scala new file mode 100644 index 000000000..1971f61df --- /dev/null +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/exception/MembershipExceededException.scala @@ -0,0 +1,11 @@ +package de.thm.ii.fbs.controller.exception + +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus + +/** + * ConflictException 400 + * @param message A human readable String + */ +@ResponseStatus(value = HttpStatus.CONFLICT) +class MembershipExceededException(message: String = "Die Gruppe ist voll.") extends RuntimeException(message) \ No newline at end of file From eb98f886bd547716c3fb640cea24ec903d6f4660 Mon Sep 17 00:00:00 2001 From: Sophie Methe Date: Sun, 12 May 2024 16:32:42 +0200 Subject: [PATCH 09/10] Correct lint errors (#1632) --- .../ii/fbs/controller/GroupController.scala | 37 ++++++++-------- .../GroupRegistrationController.scala | 32 ++++++-------- .../MembershipExceededException.scala | 2 +- .../scala/de/thm/ii/fbs/model/Group.scala | 4 +- .../GroupRegistrationService.scala | 42 +++++++++---------- .../services/persistence/GroupService.scala | 10 ++--- 6 files changed, 57 insertions(+), 70 deletions(-) diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupController.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupController.scala index 4bae1aec2..46b734ca1 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupController.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupController.scala @@ -9,11 +9,9 @@ import de.thm.ii.fbs.util.JsonWrapper._ import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import org.springframework.beans.factory.annotation.Autowired -import org.springframework.dao.DataIntegrityViolationException import org.springframework.http.MediaType import org.springframework.web.bind.annotation._ - /** * Controller to manage rest api calls for a group resource. */ @@ -23,9 +21,9 @@ import org.springframework.web.bind.annotation._ @RequestMapping (path = Array("/api/v1/courses/{cid}/groups"), produces = Array(MediaType.APPLICATION_JSON_VALUE)) class GroupController{ @Autowired - private val groupService : GroupService = null + private val groupService: GroupService = null @Autowired - private val authService : AuthService = null + private val authService: AuthService = null @Autowired private val courseRegistrationService: CourseRegistrationService = null @@ -40,12 +38,13 @@ class GroupController{ */ @GetMapping(value = Array("")) @ResponseBody - def getAll(@PathVariable ("cid") cid : Integer, @RequestParam(value = "visible", required = false) ignoreHidden: Boolean,req: HttpServletRequest, res: HttpServletResponse): List[Group] = { + def getAll(@PathVariable ("cid") cid: Integer, @RequestParam(value = "visible", required = false) + ignoreHidden: Boolean, req: HttpServletRequest, res: HttpServletResponse): List[Group] = { val user = authService.authorize(req, res) val someCourseRole = courseRegistrationService.getParticipants(cid).find(_.user.id == user.id).map(_.role) (user.globalRole, someCourseRole) match { - case (GlobalRole.ADMIN | GlobalRole.MODERATOR, _) | (_, Some(CourseRole.DOCENT)) => - val groupList = groupService.getAll(cid, false) + case (GlobalRole.ADMIN | GlobalRole.MODERATOR, _) | (_, Some(CourseRole.DOCENT)) => + val groupList = groupService.getAll(cid, ignoreHidden = false) groupList case _ => throw new ForbiddenException() } @@ -62,17 +61,17 @@ class GroupController{ */ @PostMapping(value = Array(""), consumes = Array(MediaType.APPLICATION_JSON_VALUE)) @ResponseBody - def create(@PathVariable ("cid") cid : Int, req: HttpServletRequest, res: HttpServletResponse, @RequestBody body: JsonNode): Group = { + def create(@PathVariable ("cid") cid: Int, req: HttpServletRequest, res: HttpServletResponse, @RequestBody body: JsonNode): Group = { val user = authService.authorize(req, res) val someCourseRole = courseRegistrationService.getParticipants(cid).find(_.user.id == user.id).map(_.role) - if (!(user.globalRole == GlobalRole.ADMIN || user.globalRole == GlobalRole.MODERATOR || someCourseRole == Some(CourseRole.DOCENT))) { + if (!(user.globalRole == GlobalRole.ADMIN || user.globalRole == GlobalRole.MODERATOR || someCourseRole.contains(CourseRole.DOCENT))) { throw new ForbiddenException() } val name = Option(body.get("name")).map(_.asText()) val membership = Option(body.get("membership")).map(_.asInt()) val visible = Option(body.get("visible")).map(_.asBoolean()) - (name, membership, visible) match { - case (Some(name), Some (membership), Some(visible)) + (name, membership, visible) match { + case (Some(name), Some (membership), Some(visible)) => groupService.create(Group(0, cid, name, membership, visible)) case _ => throw new BadRequestException("Malformed Request Body") } @@ -89,12 +88,12 @@ class GroupController{ */ @GetMapping(value = Array("/{gid}")) @ResponseBody - def getOne(@PathVariable ("cid") cid : Integer, @PathVariable("gid") gid: Integer, req: HttpServletRequest, res: HttpServletResponse): Group = { + def getOne(@PathVariable ("cid") cid: Integer, @PathVariable("gid") gid: Integer, req: HttpServletRequest, res: HttpServletResponse): Group = { val user = authService.authorize(req, res) val someCourseRole = courseRegistrationService.getParticipants(cid).find(_.user.id == user.id).map(_.role) groupService.get(cid, gid) match { - case Some(group) => if (!(user.globalRole == GlobalRole.ADMIN || user.globalRole == GlobalRole.MODERATOR || someCourseRole == Some(CourseRole.DOCENT))) { + case Some(group) => if (!(user.globalRole == GlobalRole.ADMIN || user.globalRole == GlobalRole.MODERATOR || someCourseRole.contains(CourseRole.DOCENT))) { throw new ForbiddenException() } else { group @@ -113,18 +112,18 @@ class GroupController{ * @param body Request Body */ @PutMapping(value = Array("/{gid}")) - def update(@PathVariable ("cid") cid : Integer, @PathVariable("gid") gid: Integer, req: HttpServletRequest, res: HttpServletResponse, + def update(@PathVariable ("cid") cid: Integer, @PathVariable("gid") gid: Integer, req: HttpServletRequest, res: HttpServletResponse, @RequestBody body: JsonNode): Unit = { val user = authService.authorize(req, res) val someCourseRole = courseRegistrationService.getParticipants(cid).find(_.user.id == user.id).map(_.role) - + (user.globalRole, someCourseRole) match { case (GlobalRole.ADMIN | GlobalRole.MODERATOR, _) | (_, Some(CourseRole.DOCENT)) => (body.retrive("name").asText(), body.retrive("membership").asInt(), body.retrive("visible").asBool() ) match { - case (Some(name), Some (membership), visible) + case (Some(name), Some (membership), visible) => groupService.update(cid, gid, Group(gid, cid, name, membership, visible.getOrElse(true))) case _ => throw new BadRequestException("Malformed Request Body") } @@ -135,13 +134,13 @@ class GroupController{ /** * Delete course * - * @param cid Course id + * @param cid Course id * @param gid Group id * @param req http request * @param res http response */ @DeleteMapping(value = Array("/{gid}")) - def delete(@PathVariable ("cid") cid : Integer, @PathVariable("gid") gid: Integer, req: HttpServletRequest, res: HttpServletResponse): Unit = { + def delete(@PathVariable ("cid") cid: Integer, @PathVariable("gid") gid: Integer, req: HttpServletRequest, res: HttpServletResponse): Unit = { val user = authService.authorize(req, res) val someCourseRole = courseRegistrationService.getParticipants(cid).find(_.user.id == user.id).map(_.role) @@ -151,4 +150,4 @@ class GroupController{ case _ => throw new ForbiddenException() } } -} \ No newline at end of file +} diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupRegistrationController.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupRegistrationController.scala index b86bdefe4..7de518adb 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupRegistrationController.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupRegistrationController.scala @@ -1,15 +1,12 @@ package de.thm.ii.fbs.controller -import com.fasterxml.jackson.databind.JsonNode -import de.thm.ii.fbs.controller.exception.{BadRequestException, ForbiddenException, ResourceNotFoundException, MembershipExceededException} -import de.thm.ii.fbs.model.{Group, CourseRole, GlobalRole, User, Participant} +import de.thm.ii.fbs.controller.exception.{ForbiddenException, MembershipExceededException, ResourceNotFoundException} +import de.thm.ii.fbs.model.{CourseRole, GlobalRole, Group, Participant} import de.thm.ii.fbs.services.persistence._ import de.thm.ii.fbs.services.security.AuthService -import de.thm.ii.fbs.util.JsonWrapper._ import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import org.springframework.beans.factory.annotation.Autowired -import org.springframework.dao.DataIntegrityViolationException import org.springframework.http.MediaType import org.springframework.web.bind.annotation._ @@ -32,13 +29,12 @@ class GroupRegistrationController { /** * Add a user to a group within a course - * + * * @param cid Course id * @param gid Group id * @param uid User id * @param req http request * @param res http response - * @param body Content */ @PutMapping(value = Array("/courses/{cid}/groups/{gid}/users/{uid}"), consumes = Array(MediaType.APPLICATION_JSON_VALUE)) def addUserToGroup(@PathVariable("cid") cid: Int, @PathVariable("gid") gid: Int, @PathVariable("uid") uid: Int, @@ -48,16 +44,16 @@ class GroupRegistrationController { val hasCoursePrivileges = courseRegistrationService.getCoursePrivileges(user.id).getOrElse(cid, CourseRole.STUDENT) == CourseRole.DOCENT if (hasGlobalPrivileges || hasCoursePrivileges || user.id == uid) { //Check if the group is full - val currentMembership = groupRegistrationService.getGroupMembership(cid,gid) + val currentMembership = groupRegistrationService.getGroupMembership(cid, gid) val group = groupService.get(cid, gid) group match { case Some(group) => val maxMembership: Int = group.membership if (currentMembership < maxMembership) { groupRegistrationService.addUserToGroup(uid, cid, gid) } else { - throw new MembershipExceededException() + throw new MembershipExceededException() } - case _ => throw new ResourceNotFoundException() + case _ => throw new ResourceNotFoundException() } } else { throw new ForbiddenException() @@ -66,7 +62,7 @@ class GroupRegistrationController { /** * Remove a user from a group - * + * * @param uid User id * @param cid Course id * @param gid Group id @@ -74,7 +70,8 @@ class GroupRegistrationController { * @param res http response */ @DeleteMapping(value = Array("/courses/{cid}/groups/{gid}/users/{uid}")) - def removeUserFromGroup(@PathVariable("uid") uid: Int, @PathVariable("cid") cid: Int, @PathVariable("gid") gid: Int, req: HttpServletRequest, res: HttpServletResponse): Unit = { + def removeUserFromGroup(@PathVariable("uid") uid: Int, @PathVariable("cid") cid: Int, @PathVariable("gid") gid: Int, + req: HttpServletRequest, res: HttpServletResponse): Unit = { val user = authService.authorize(req, res) val hasGlobalPrivileges = user.hasRole(GlobalRole.ADMIN, GlobalRole.MODERATOR) val hasCoursePrivileges = courseRegistrationService.getCoursePrivileges(user.id).getOrElse(cid, CourseRole.STUDENT) == CourseRole.DOCENT @@ -87,7 +84,7 @@ class GroupRegistrationController { /** * Remove all users from a group - * + * * @param cid Course id * @param gid Group id * @param req http request @@ -107,7 +104,7 @@ class GroupRegistrationController { /** * Retrieve all groups of a specific user - * + * * @param uid User id * @param req http request * @param res http response @@ -131,7 +128,7 @@ class GroupRegistrationController { * @param gid Group id * @param req http request * @param res http response - * @return List of course participants + * @return List of course participants */ @GetMapping(value = Array("/courses/{cid}/groups/{gid}/participants")) @ResponseBody @@ -146,8 +143,3 @@ class GroupRegistrationController { } } } - - - - - diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/exception/MembershipExceededException.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/exception/MembershipExceededException.scala index 1971f61df..3d5d6ac77 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/exception/MembershipExceededException.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/exception/MembershipExceededException.scala @@ -8,4 +8,4 @@ import org.springframework.web.bind.annotation.ResponseStatus * @param message A human readable String */ @ResponseStatus(value = HttpStatus.CONFLICT) -class MembershipExceededException(message: String = "Die Gruppe ist voll.") extends RuntimeException(message) \ No newline at end of file +class MembershipExceededException(message: String = "Die Gruppe ist voll.") extends RuntimeException(message) diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Group.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Group.scala index 1aa690cd5..bf4630690 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Group.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Group.scala @@ -6,7 +6,7 @@ package de.thm.ii.fbs.model * @param courseId course to which the group belongs * @param name Name of the group * @param membership The max number of members - * @param visible The visibility of the group, false = invisible + * @param visible The visibility of the group, false = invisible */ -case class Group(id: Int, courseId: Int, name: String, membership: Int, visible: Boolean = true) \ No newline at end of file +case class Group(id: Int, courseId: Int, name: String, membership: Int, visible: Boolean = true) diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupRegistrationService.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupRegistrationService.scala index 7d5d8a632..e047bc8ba 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupRegistrationService.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupRegistrationService.scala @@ -19,34 +19,34 @@ class GroupRegistrationService { /** * Add a user to a group - * + * * @param uid User id * @param cid Course id * @param gid Group id * @return True if successful */ - def addUserToGroup(uid:Int, cid: Int, gid: Int): Boolean = - 1 == DB.update("INSERT INTO user_group (user_id, course_id, group_id) VALUES (?,?,?);",uid, cid, gid) + def addUserToGroup(uid: Int, cid: Int, gid: Int): Boolean = + 1 == DB.update("INSERT INTO user_group (user_id, course_id, group_id) VALUES (?,?,?);", uid, cid, gid) /** * Remove a user from a group * * @param uid User id * @param cid Course id - * @param gid Group id - * @return True if sucessfully deregistered + * @param gid Group id + * @return True if successfully deregistered */ def removeUserFromGroup(uid: Int, cid: Int, gid: Int): Boolean = - 1 == DB.update("DELETE FROM user_group WHERE user_id = ? AND course_id = ? AND group_id = ?",uid, cid, gid) + 1 == DB.update("DELETE FROM user_group WHERE user_id = ? AND course_id = ? AND group_id = ?", uid, cid, gid) /** * Remove all users from a group * * @param cid Course id - * @param gid Group id - * @return True if sucessfully deregistered + * @param gid Group id + * @return True if successfully deregistered */ - def removeAllUsersFromGroup(cid: Int, gid: Int): Boolean = + def removeAllUsersFromGroup(cid: Int, gid: Int): Boolean = 1 == DB.update("DELETE FROM user_group WHERE course_id = ? AND group_id = ?", cid, gid) /** @@ -57,7 +57,8 @@ class GroupRegistrationService { * @return List of groups */ def getUserGroups(uid: Int, ignoreHidden: Boolean = true): List[Group] = DB.query( - "SELECT g.group_id, g.course_id, g.name, g.membership, g.visible FROM `group` g JOIN user_group ug ON g.group_id = ug.group_id WHERE ug.user_id = ? ORDER BY g.course_id ASC" + "SELECT g.group_id, g.course_id, g.name, g.membership, g.visible " + + "FROM `group` g JOIN user_group ug ON g.group_id = ug.group_id WHERE ug.user_id = ? ORDER BY g.course_id ASC" + (if (ignoreHidden) " AND g.visible = 1" else ""), (res, _) => parseResult(res), uid) @@ -66,7 +67,7 @@ class GroupRegistrationService { * * @param cid Course id * @param gid Group id - * @return List of members + * @return List of members */ def getMembers(cid: Int, gid: Int): List[Participant] = DB.query( "SELECT u.user_id, u.prename, u.surname, u.email, u.username, u.alias, u.global_role, uc.course_role " + @@ -78,17 +79,17 @@ class GroupRegistrationService { /** * Gets current number of members of a group - * + * * @param cid Course id * @param gid Group id - * @return Number of members + * @return Number of members */ - def getGroupMembership(cid: Int, gid: Int) : Int = { - val groupMembershipRowMapper: RowMapper[Int] = (rs: ResultSet, rowNum: Int) => rs.getInt(1) + def getGroupMembership(cid: Int, gid: Int): Int = { + val groupMembershipRowMapper: RowMapper[Int] = (rs: ResultSet, _) => rs.getInt(1) val sql = "SELECT COUNT(*) FROM user_group WHERE course_id = ? AND group_id = ?" - return jdbc.queryForObject(sql, groupMembershipRowMapper, cid, gid); - } - + jdbc.queryForObject(sql, groupMembershipRowMapper, cid, gid) + } + private def parseResult(res: ResultSet): Group = Group( id = res.getInt("group_id"), courseId = res.getInt("course_id"), @@ -96,7 +97,7 @@ class GroupRegistrationService { membership = res.getInt("membership"), visible = res.getBoolean("visible"), ) - + private def parseUserResult(res: ResultSet): User = new User( prename = res.getString("prename"), surname = res.getString("surname"), @@ -107,6 +108,3 @@ class GroupRegistrationService { id = res.getInt("user_id") ) } - - - \ No newline at end of file diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupService.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupService.scala index 87ce67f4d..5d3fd8ec4 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupService.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupService.scala @@ -10,7 +10,7 @@ import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Component /** - * Handles the creation, deletion and modifications of groups persistant state. + * Handles the creation, deletion and modifications of groups persistent state. */ @Component class GroupService{ @@ -28,7 +28,7 @@ class GroupService{ s"SELECT group_id, course_id, name, membership, visible FROM `group` WHERE" + (if (ignoreHidden) " visible = 1 AND" else "") + s" course_id = $cid", (res, _) => parseResult(res) ) - + /** * Create a new group * @@ -44,7 +44,6 @@ class GroupService{ } } - /** * Get a single group by id * @@ -56,7 +55,6 @@ class GroupService{ "SELECT group_id, course_id, name, membership, visible FROM `group` WHERE course_id = ? AND group_id = ?", (res, _) => parseResult(res), cid, gid).headOption - /** * Update a single group by id * @@ -66,7 +64,8 @@ class GroupService{ * @return True if successful */ def update(cid: Int, gid: Int, group: Group): Boolean = { - 1 == DB.update("UPDATE `group` SET name = ?, membership = ?, visible = ? WHERE course_id = ? AND group_id = ?", group.name, group.membership, group.visible,cid, gid) + 1 == DB.update("UPDATE `group` SET name = ?, membership = ?, visible = ? WHERE course_id = ? AND group_id = ?", + group.name, group.membership, group.visible, cid, gid) } /** @@ -78,7 +77,6 @@ class GroupService{ */ def delete(cid: Int, gid: Int): Boolean = 1 == DB.update("DELETE FROM `group` WHERE course_id = ? AND group_id = ?", cid, gid) - private def parseResult(res: ResultSet): Group = Group( id = res.getInt("group_id"), courseId = res.getInt("course_id"), From 3d8d457b9192e0bb57c8722181ebe35affde0162 Mon Sep 17 00:00:00 2001 From: Jonas Kuche Date: Tue, 28 May 2024 08:34:19 +0200 Subject: [PATCH 10/10] docs(core): update api docs with group routes --- modules/fbs-core/api/api-docs.yml | 318 +++++++++++++++++++++++++++++- 1 file changed, 314 insertions(+), 4 deletions(-) diff --git a/modules/fbs-core/api/api-docs.yml b/modules/fbs-core/api/api-docs.yml index 1d5858e54..01494dfd7 100644 --- a/modules/fbs-core/api/api-docs.yml +++ b/modules/fbs-core/api/api-docs.yml @@ -19,6 +19,10 @@ tags: description: The Semester api - name: Course Registration description: The course registration api + - name: Group + description: The group API + - name: GroupMembership + description: The group Membership - name: Task description: The task api - name: Course evaluation @@ -859,6 +863,287 @@ paths: description: Unauthorized "403": description: Forbidden + /courses/{cid}/groups: + get: + operationId: getGroups + tags: + - Group + summary: Get a group list + parameters: + - name: cid + in: path + required: true + schema: + type: integer + - name: visible + in: query + required: false + schema: + type: boolean + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Group' + '403': + description: Forbidden + post: + operationId: createGroup + tags: + - Group + summary: Create a new group + parameters: + - name: cid + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GroupRequest' + responses: + '200': + description: Group created + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + '400': + description: Bad Request + '403': + description: Forbidden + /courses/{cid}/groups/{gid}: + get: + operationId: getGroup + tags: + - Group + summary: Get a single group by id + parameters: + - name: cid + in: path + required: true + schema: + type: integer + - name: gid + in: path + required: true + schema: + type: integer + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + '403': + description: Forbidden + '404': + description: Not Found + put: + operationId: updateGroup + tags: + - Group + summary: Update a single group by id + parameters: + - name: cid + in: path + required: true + schema: + type: integer + - name: gid + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GroupRequest' + responses: + '204': + description: No Content + '400': + description: Bad Request + '403': + description: Forbidden + delete: + operationId: deleteGroup + tags: + - Group + summary: Delete a group by id + parameters: + - name: cid + in: path + required: true + schema: + type: integer + - name: gid + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + '403': + description: Forbidden + /courses/{cid}/groups/{gid}/users/{uid}: + put: + tags: + - GroupMembership + summary: Add a user to a group within a course + operationId: addUserToGroup + parameters: + - name: cid + in: path + required: true + schema: + type: integer + description: Course ID + - name: gid + in: path + required: true + schema: + type: integer + description: Group ID + - name: uid + in: path + required: true + schema: + type: integer + description: User ID + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + '200': + description: User added to the group + '403': + description: Forbidden + '404': + description: Resource not found + '409': + description: Membership exceeded + delete: + tags: + - GroupMembership + summary: Remove a user from a group + operationId: removeUserFromGroup + parameters: + - name: cid + in: path + required: true + schema: + type: integer + description: Course ID + - name: gid + in: path + required: true + schema: + type: integer + description: Group ID + - name: uid + in: path + required: true + schema: + type: integer + description: User ID + responses: + '200': + description: User removed from the group + '403': + description: Forbidden + /courses/{cid}/groups/{gid}/users: + delete: + tags: + - GroupMembership + summary: Remove all users from a group + operationId: removeAllUsersFromGroup + parameters: + - name: cid + in: path + required: true + schema: + type: integer + description: Course ID + - name: gid + in: path + required: true + schema: + type: integer + description: Group ID + responses: + '200': + description: All users removed from the group + '403': + description: Forbidden + /users/{uid}/groups: + get: + tags: + - GroupMembership + summary: Retrieve all groups of a specific user + operationId: getUserGroups + parameters: + - name: uid + in: path + required: true + schema: + type: integer + description: User ID + responses: + '200': + description: List of groups + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Group' + '403': + description: Forbidden + /courses/{cid}/groups/{gid}/participants: + get: + tags: + - GroupMembership + summary: Get all course participants who are part of a group + operationId: getGroupParticipants + parameters: + - name: cid + in: path + required: true + schema: + type: integer + description: Course ID + - name: gid + in: path + required: true + schema: + type: integer + description: Group ID + responses: + '200': + description: List of course participants + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + '403': + description: Forbidden "/courses/{cid}/tasks": get: operationId: getCourseTasks @@ -3197,10 +3482,9 @@ components: required: true securitySchemes: JWT: - type: apiKey - in: header - name: Authorization - description: Bearer + type: http + scheme: bearer + bearerFormat: JWT schemas: User: type: object @@ -3261,6 +3545,32 @@ components: type: integer name: type: string + Group: + type: object + properties: + id: + type: integer + cid: + type: integer + name: + type: string + membership: + type: integer + visible: + type: boolean + GroupRequest: + type: object + properties: + name: + type: string + membership: + type: integer + visible: + type: boolean + required: + - name + - membership + - visible Task: type: object required: