diff --git a/.github/workflows/check-fbs-module.yml b/.github/workflows/check-fbs-module.yml index 89ca620c0..52563bccf 100644 --- a/.github/workflows/check-fbs-module.yml +++ b/.github/workflows/check-fbs-module.yml @@ -97,12 +97,12 @@ jobs: name: Run ${{ inputs.name }} Tests with: arguments: ${{ inputs.testArguments }} - - uses: gradle/gradle-build-action@v2 - name: Generate ${{ inputs.name }} Coverage - if: ${{ inputs.coverageArguments }} - with: - arguments: ${{ inputs.coverageArguments }} # Disabled because of coverals errors +# - uses: gradle/gradle-build-action@v2 +# name: Generate ${{ inputs.name }} Coverage +# if: ${{ inputs.coverageArguments }} +# with: +# arguments: ${{ inputs.coverageArguments }} # - uses: ./.github/actions/retry-codecov # if: ${{ inputs.coverageFilesLocation }} # name: Upload Coverage Report diff --git a/docker-compose.yml b/docker-compose.yml index 5f9eab7c3..bfdc67833 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,8 @@ services: depends_on: - mysql1 - runner + - mongodb + - minio ports: - "127.0.0.1:443:443" - "127.0.0.1:80:80" 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: 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; + + 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; + 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..46b734ca1 --- /dev/null +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupController.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} +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.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, ignoreHidden = 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.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)) + => 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.contains(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() + } + } +} 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..7de518adb --- /dev/null +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/GroupRegistrationController.scala @@ -0,0 +1,145 @@ +package de.thm.ii.fbs.controller + +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 javax.servlet.http.{HttpServletRequest, HttpServletResponse} +import org.springframework.beans.factory.annotation.Autowired +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 + */ + @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() + } + } +} 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..3d5d6ac77 --- /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) 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..bf4630690 --- /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) 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..e047bc8ba --- /dev/null +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupRegistrationService.scala @@ -0,0 +1,110 @@ +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 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) + + /** + * Remove all users from a group + * + * @param cid Course id + * @param gid Group id + * @return True if successfully 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, _) => rs.getInt(1) + val sql = "SELECT COUNT(*) FROM user_group WHERE course_id = ? AND group_id = ?" + 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") + ) +} 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..5d3fd8ec4 --- /dev/null +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/GroupService.scala @@ -0,0 +1,87 @@ +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 persistent 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"), + ) +} diff --git a/modules/fbs-core/web/src/app/page-components/sql-playground/sql-input-tabs/highlighted-input/highlighted-input.component.ts b/modules/fbs-core/web/src/app/page-components/sql-playground/sql-input-tabs/highlighted-input/highlighted-input.component.ts index 84a194572..80e0b15cf 100644 --- a/modules/fbs-core/web/src/app/page-components/sql-playground/sql-input-tabs/highlighted-input/highlighted-input.component.ts +++ b/modules/fbs-core/web/src/app/page-components/sql-playground/sql-input-tabs/highlighted-input/highlighted-input.component.ts @@ -146,7 +146,7 @@ export class HighlightedInputComponent // allow only caracteres for valid sql query text = text.replace( - /[^a-zA-Z0-9\(\)\[\]\{\}\s\.\,\;\=\+\-\*\/\>\<\!\@\#\$\%\^\&\_\~\`\:\'\"\|\\]/g, + /[^a-zA-Z0-9üöäÄÖÜß\(\)\[\]\{\}\s\.\,\;\=\+\-\*\/\>\<\!\@\#\$\?\%\^\&\_\~\`´°²³§\:\'\"\|\\]/g, "" );