Skip to content

Commit

Permalink
Communication: Add FAQs to Artemis (#9325)
Browse files Browse the repository at this point in the history
  • Loading branch information
cremertim authored Oct 3, 2024
1 parent ad160ff commit c3ab176
Show file tree
Hide file tree
Showing 55 changed files with 2,507 additions and 20 deletions.
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ module.exports = {
coverageThreshold: {
global: {
// TODO: in the future, the following values should increase to at least 90%
statements: 87.35,
branches: 73.57,
functions: 81.91,
lines: 87.41,
statements: 87.36,
branches: 73.52,
functions: 81.9,
lines: 87.42,
},
},
coverageReporters: ['clover', 'json', 'lcov', 'text-summary'],
Expand Down
99 changes: 99 additions & 0 deletions src/main/java/de/tum/cit/aet/artemis/communication/domain/Faq.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package de.tum.cit.aet.artemis.communication.domain;

import java.util.HashSet;
import java.util.Set;

import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;

import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.core.domain.AbstractAuditingEntity;
import de.tum.cit.aet.artemis.core.domain.Course;

/**
* A FAQ.
*/
@Entity
@Table(name = "faq")
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class Faq extends AbstractAuditingEntity {

@Column(name = "question_title")
private String questionTitle;

@Column(name = "question_answer")
private String questionAnswer;

@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "faq_category", joinColumns = @JoinColumn(name = "faq_id"))
@Column(name = "category")
private Set<String> categories = new HashSet<>();

@Enumerated(EnumType.STRING)
@Column(name = "faq_state")
private FaqState faqState;

@ManyToOne
@JsonIgnoreProperties(value = { "faqs" }, allowSetters = true)
private Course course;

public String getQuestionTitle() {
return questionTitle;
}

public void setQuestionTitle(String questionTitle) {
this.questionTitle = questionTitle;
}

public String getQuestionAnswer() {
return questionAnswer;
}

public void setQuestionAnswer(String questionAnswer) {
this.questionAnswer = questionAnswer;
}

public Course getCourse() {
return course;
}

public void setCourse(Course course) {
this.course = course;
}

public Set<String> getCategories() {
return categories;
}

public void setCategories(Set<String> categories) {
this.categories = categories;
}

public FaqState getFaqState() {
return faqState;
}

public void setFaqState(FaqState faqState) {
this.faqState = faqState;
}

@Override
public String toString() {
return "Faq{" + "id=" + getId() + ", questionTitle='" + getQuestionTitle() + "'" + ", questionAnswer='" + getQuestionAnswer() + "'" + ", faqState='" + getFaqState() + "}";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.tum.cit.aet.artemis.communication.domain;

public enum FaqState {
ACCEPTED, REJECTED, PROPOSED
}
17 changes: 17 additions & 0 deletions src/main/java/de/tum/cit/aet/artemis/communication/dto/FaqDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package de.tum.cit.aet.artemis.communication.dto;

import java.util.Set;

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.communication.domain.Faq;
import de.tum.cit.aet.artemis.communication.domain.FaqState;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record FaqDTO(Long id, String questionTitle, String questionAnswer, Set<String> categories, FaqState faqState) {

public FaqDTO(Faq faq) {
this(faq.getId(), faq.getQuestionTitle(), faq.getQuestionAnswer(), faq.getCategories(), faq.getFaqState());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package de.tum.cit.aet.artemis.communication.repository;

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.util.Set;

import org.springframework.context.annotation.Profile;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import de.tum.cit.aet.artemis.communication.domain.Faq;
import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository;

/**
* Spring Data repository for the Faq entity.
*/
@Profile(PROFILE_CORE)
@Repository
public interface FaqRepository extends ArtemisJpaRepository<Faq, Long> {

Set<Faq> findAllByCourseId(Long courseId);

@Query("""
SELECT DISTINCT faq.categories
FROM Faq faq
WHERE faq.course.id = :courseId
""")
Set<String> findAllCategoriesByCourseId(@Param("courseId") Long courseId);

@Transactional
@Modifying
void deleteAllByCourseId(Long courseId);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package de.tum.cit.aet.artemis.communication.web;

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import de.tum.cit.aet.artemis.communication.domain.Faq;
import de.tum.cit.aet.artemis.communication.dto.FaqDTO;
import de.tum.cit.aet.artemis.communication.repository.FaqRepository;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.repository.CourseRepository;
import de.tum.cit.aet.artemis.core.security.Role;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent;
import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService;
import de.tum.cit.aet.artemis.core.util.HeaderUtil;

/**
* REST controller for managing Faqs.
*/
@Profile(PROFILE_CORE)
@RestController
@RequestMapping("api/")
public class FaqResource {

private static final Logger log = LoggerFactory.getLogger(FaqResource.class);

private static final String ENTITY_NAME = "faq";

@Value("${jhipster.clientApp.name}")
private String applicationName;

private final CourseRepository courseRepository;

private final AuthorizationCheckService authCheckService;

private final FaqRepository faqRepository;

public FaqResource(CourseRepository courseRepository, AuthorizationCheckService authCheckService, FaqRepository faqRepository) {

this.courseRepository = courseRepository;
this.authCheckService = authCheckService;
this.faqRepository = faqRepository;
}

/**
* POST /courses/:courseId/faqs : Create a new faq.
*
* @param faq the faq to create *
* @param courseId the id of the course the faq belongs to
* @return the ResponseEntity with status 201 (Created) and with body the new faq, or with status 400 (Bad Request)
* if the faq has already an ID or if the faq course id does not match with the path variable
* @throws URISyntaxException if the Location URI syntax is incorrect
*/
@PostMapping("courses/{courseId}/faqs")
@EnforceAtLeastInstructor
public ResponseEntity<FaqDTO> createFaq(@RequestBody Faq faq, @PathVariable Long courseId) throws URISyntaxException {
log.debug("REST request to save Faq : {}", faq);
if (faq.getId() != null) {
throw new BadRequestAlertException("A new faq cannot already have an ID", ENTITY_NAME, "idExists");
}

if (faq.getCourse() == null || !faq.getCourse().getId().equals(courseId)) {
throw new BadRequestAlertException("Course ID in path and FAQ do not match", ENTITY_NAME, "courseIdMismatch");
}
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null);

Faq savedFaq = faqRepository.save(faq);
FaqDTO dto = new FaqDTO(savedFaq);
return ResponseEntity.created(new URI("/api/courses/" + courseId + "/faqs/" + savedFaq.getId())).body(dto);
}

/**
* PUT /courses/:courseId/faqs/:faqId : Updates an existing faq.
*
* @param faq the faq to update
* @param faqId id of the faq to be updated *
* @param courseId the id of the course the faq belongs to
* @return the ResponseEntity with status 200 (OK) and with body the updated faq, or with status 400 (Bad Request)
* if the faq is not valid or if the faq course id does not match with the path variable
*/
@PutMapping("courses/{courseId}/faqs/{faqId}")
@EnforceAtLeastInstructor
public ResponseEntity<FaqDTO> updateFaq(@RequestBody Faq faq, @PathVariable Long faqId, @PathVariable Long courseId) {
log.debug("REST request to update Faq : {}", faq);
if (faqId == null || !faqId.equals(faq.getId())) {
throw new BadRequestAlertException("Id of FAQ and path must match", ENTITY_NAME, "idNull");
}
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null);
Faq existingFaq = faqRepository.findByIdElseThrow(faqId);
if (!Objects.equals(existingFaq.getCourse().getId(), courseId)) {
throw new BadRequestAlertException("Course ID of the FAQ provided courseID must match", ENTITY_NAME, "idNull");
}
Faq updatedFaq = faqRepository.save(faq);
FaqDTO dto = new FaqDTO(updatedFaq);
return ResponseEntity.ok().body(dto);
}

/**
* GET /courses/:courseId/faqs/:faqId : get the faq with the id faqId.
*
* @param faqId the faqId of the faq to retrieve *
* @param courseId the id of the course the faq belongs to
* @return the ResponseEntity with status 200 (OK) and with body the faq, or with status 404 (Not Found)
*/
@GetMapping("courses/{courseId}/faqs/{faqId}")
@EnforceAtLeastStudent
public ResponseEntity<FaqDTO> getFaq(@PathVariable Long faqId, @PathVariable Long courseId) {
log.debug("REST request to get faq {}", faqId);
Faq faq = faqRepository.findByIdElseThrow(faqId);
if (faq.getCourse() == null || !faq.getCourse().getId().equals(courseId)) {
throw new BadRequestAlertException("Course ID in path and FAQ do not match", ENTITY_NAME, "courseIdMismatch");
}
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, faq.getCourse(), null);
FaqDTO dto = new FaqDTO(faq);
return ResponseEntity.ok(dto);
}

/**
* DELETE /courses/:courseId/faqs/:faqId : delete the "id" faq.
*
* @param faqId the id of the faq to delete
* @param courseId the id of the course the faq belongs to
* @return the ResponseEntity with status 200 (OK)
*/
@DeleteMapping("courses/{courseId}/faqs/{faqId}")
@EnforceAtLeastInstructor
public ResponseEntity<Void> deleteFaq(@PathVariable Long faqId, @PathVariable Long courseId) {

log.debug("REST request to delete faq {}", faqId);
Faq existingFaq = faqRepository.findByIdElseThrow(faqId);
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, existingFaq.getCourse(), null);
if (!Objects.equals(existingFaq.getCourse().getId(), courseId)) {
throw new BadRequestAlertException("Course ID of the FAQ provided courseID must match", ENTITY_NAME, "idNull");
}
faqRepository.deleteById(faqId);
return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, faqId.toString())).build();
}

/**
* GET /courses/:courseId/faqs : get all the faqs of a course
*
* @param courseId the courseId of the course for which all faqs should be returned
* @return the ResponseEntity with status 200 (OK) and the list of faqs in body
*/
@GetMapping("courses/{courseId}/faqs")
@EnforceAtLeastStudent
public ResponseEntity<Set<FaqDTO>> getFaqForCourse(@PathVariable Long courseId) {
log.debug("REST request to get all Faqs for the course with id : {}", courseId);

Course course = courseRepository.findByIdElseThrow(courseId);
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null);
Set<Faq> faqs = faqRepository.findAllByCourseId(courseId);
Set<FaqDTO> faqDTOS = faqs.stream().map(FaqDTO::new).collect(Collectors.toSet());
return ResponseEntity.ok().body(faqDTOS);
}

/**
* GET /courses/:courseId/faq-categories : get all the faq categories of a course
*
* @param courseId the courseId of the course for which all faq categories should be returned
* @return the ResponseEntity with status 200 (OK) and the list of faqs in body
*/
@GetMapping("courses/{courseId}/faq-categories")
@EnforceAtLeastStudent
public ResponseEntity<Set<String>> getFaqCategoriesForCourse(@PathVariable Long courseId) {
log.debug("REST request to get all Faq Categories for the course with id : {}", courseId);

Course course = courseRepository.findByIdElseThrow(courseId);
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null);
Set<String> faqs = faqRepository.findAllCategoriesByCourseId(courseId);

return ResponseEntity.ok().body(faqs);
}
}
Loading

0 comments on commit c3ab176

Please sign in to comment.