Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Implement FAQ system to Artemis #9325

Open
wants to merge 74 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
b3bf2bb
FAQ - system provisional backend
cremertim Sep 3, 2024
44a7060
Merge branch 'refs/heads/develop' into feature/faq/implement-faq-basis
cremertim Sep 3, 2024
f49731e
Add meta information and state to FAQ
cremertim Sep 3, 2024
c2efdbb
Fixed minor mapping error
cremertim Sep 3, 2024
5467a10
Added cascade delete
cremertim Sep 5, 2024
ebe8044
Added cascade deletion on course deletion
cremertim Sep 9, 2024
ae7e4b4
Merge branch 'refs/heads/develop' into feature/faq/implement-faq-basis
cremertim Sep 9, 2024
2d3ea4e
Changed rest of server stuff
cremertim Sep 10, 2024
be22131
Add translations and fix uppercase
cremertim Sep 10, 2024
407072e
Add first draft of FAQ System
cremertim Sep 10, 2024
f96f610
refactored toggleFilters to make commits work
cremertim Sep 10, 2024
af392db
Added integration test, but they do not work yet
cremertim Sep 11, 2024
1c29060
Integration Tests
cremertim Sep 11, 2024
943363d
Merge branch 'develop' into feature/faq/implement-faq-basis
cremertim Sep 11, 2024
6e80b3b
Integration Tests fixed. Why so ever its works now
cremertim Sep 12, 2024
4a9609c
Formula Action change from Patrick
cremertim Sep 12, 2024
f276f9b
Make components standalone
cremertim Sep 12, 2024
a436aa8
Removed unnecessary import statements
cremertim Sep 12, 2024
117b305
Made filter to use badges, not plain text
cremertim Sep 14, 2024
ea49c58
Added Student view and filtering as a service
cremertim Sep 15, 2024
ac619d3
Add markdown highlighting for FAQ's
cremertim Sep 16, 2024
b117217
Allowed students to pull stuff
cremertim Sep 17, 2024
dee6e5a
FAQ - system provisional backend
cremertim Sep 3, 2024
8d254d2
Add meta information and state to FAQ
cremertim Sep 3, 2024
d1f7823
Fixed minor mapping error
cremertim Sep 3, 2024
af53c20
Added cascade delete
cremertim Sep 5, 2024
a5b7793
Added cascade deletion on course deletion
cremertim Sep 9, 2024
58136b6
Changed rest of server stuff
cremertim Sep 10, 2024
db67131
Add translations and fix uppercase
cremertim Sep 10, 2024
6c494e7
Add first draft of FAQ System
cremertim Sep 10, 2024
16ddd8c
refactored toggleFilters to make commits work
cremertim Sep 10, 2024
019349d
Added integration test, but they do not work yet
cremertim Sep 11, 2024
4ab0edf
Integration Tests
cremertim Sep 11, 2024
b433a3a
Integration Tests fixed. Why so ever its works now
cremertim Sep 12, 2024
a232c25
Formula Action change from Patrick
cremertim Sep 12, 2024
80cc42a
Make components standalone
cremertim Sep 12, 2024
8f71b0a
Removed unnecessary import statements
cremertim Sep 12, 2024
7ffc7bf
Made filter to use badges, not plain text
cremertim Sep 14, 2024
f68d621
Added Student view and filtering as a service
cremertim Sep 15, 2024
df84495
Add markdown highlighting for FAQ's
cremertim Sep 16, 2024
1174603
Allowed students to pull stuff
cremertim Sep 17, 2024
0b2bae7
fixed imports
cremertim Sep 17, 2024
a8bd41b
fixed imports
cremertim Sep 17, 2024
688ceb8
moved faq button up
cremertim Sep 17, 2024
0290f9b
Made page scrollable, moved categories in the same row to safe space
cremertim Sep 17, 2024
5976d6f
removed search bar
cremertim Sep 17, 2024
047eb52
Fixed style issues
cremertim Sep 17, 2024
5114dd8
Fixed coderabbit style issues
cremertim Sep 17, 2024
764e7ba
Fixed coderabbit style issues
cremertim Sep 17, 2024
20f02da
Test structure
cremertim Sep 17, 2024
6953f40
Fixed Coderabbit
cremertim Sep 18, 2024
031bc3b
Merge branch 'develop' into feature/faq/implement-faq-basis
cremertim Sep 18, 2024
80c5452
Fixed Coderabbit
cremertim Sep 18, 2024
5aeda2f
Further Coderabbit
cremertim Sep 18, 2024
f5ab0e4
improved validation and further coderabbit fixes
cremertim Sep 18, 2024
b9f0935
Fixed minor issue
cremertim Sep 18, 2024
ddca96e
Fixed to enable faq for course
cremertim Sep 18, 2024
cb12c44
Another coderabit hint
cremertim Sep 18, 2024
d767dd9
Another coderabit hint
cremertim Sep 18, 2024
b5929e3
Remove repo from resource
cremertim Sep 18, 2024
c0e1432
Remove repo from resource
cremertim Sep 19, 2024
bd8a8c8
Added client test for faq.service
cremertim Sep 19, 2024
cc9c3e5
Adjusted naming to be equal
cremertim Sep 20, 2024
722366e
Added first client tests for the components
cremertim Sep 20, 2024
afe6ea1
Rename FAQ
cremertim Sep 20, 2024
7ede184
Merge branch 'develop' into feature/faq/implement-faq-basis
cremertim Sep 20, 2024
1dfdac3
Fixed creation bug and made category filter working as expected
cremertim Sep 20, 2024
8ae8c86
added tests, one is still not working
cremertim Sep 20, 2024
ab73e50
tried to fix E2E tests
cremertim Sep 20, 2024
40e1213
coderabit
cremertim Sep 20, 2024
f33cad4
Fixed E2E test
cremertim Sep 23, 2024
aff8ad6
Merge branch 'develop' into feature/faq/implement-faq-basis
cremertim Sep 23, 2024
e80e7fd
Integrated changes, fixed last test
cremertim Sep 23, 2024
688305d
refixed e2e test
cremertim Sep 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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_categories", joinColumns = @JoinColumn(name = "faq_id"))
@Column(name = "categories")
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() + ", title='" + getQuestionTitle() + "'" + ", description='" + getQuestionTitle() + "'" + ", 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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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> {

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

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

@Transactional
@Modifying
@Query("""
DELETE
FROM Faq faq
WHERE faq.course.id = :courseId
""")
void deleteAllByCourseId(@Param("courseId") Long courseId);

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

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

import java.util.Optional;
import java.util.Set;

import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

import de.tum.cit.aet.artemis.communication.domain.Faq;
import de.tum.cit.aet.artemis.communication.repository.FaqRepository;

/**
* REST service for managing Faqs.
*/
@Profile(PROFILE_CORE)
@Service
public class FaqService {
cremertim marked this conversation as resolved.
Show resolved Hide resolved
cremertim marked this conversation as resolved.
Show resolved Hide resolved

private final FaqRepository faqRepository;

public FaqService(FaqRepository faqRepository) {
this.faqRepository = faqRepository;
}

/**
* Deletes the given faq
*
* @param faqId the ID of the FAQ to be deleted
*/
public void deleteById(long faqId) {
faqRepository.deleteById(faqId);

}

public Faq save(Faq faq) {
return faqRepository.save(faq);
}

public Set<String> findAllCategoriesByCourseId(long courseId) {
return faqRepository.findAllCategoriesByCourseId(courseId);
}

public Optional<Faq> findById(Long faqId) {
return faqRepository.findById(faqId);
}

public Set<Faq> findAllByCourseId(Long courseId) {
return faqRepository.findAllByCourseId(courseId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
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.Set;

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.service.FaqService;
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.exception.EntityNotFoundException;
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 {
cremertim marked this conversation as resolved.
Show resolved Hide resolved

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

private static final String ENTITY_NAME = "faq";

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

cremertim marked this conversation as resolved.
Show resolved Hide resolved
private final FaqService faqService;

private final CourseRepository courseRepository;

private final AuthorizationCheckService authCheckService;

public FaqResource(FaqService faqService, CourseRepository courseRepository, AuthorizationCheckService authCheckService) {

this.faqService = faqService;
this.courseRepository = courseRepository;
this.authCheckService = authCheckService;
}

/**
* POST /faqs : Create a new faq.
*
* @param faq the faq to create
* @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
* @throws URISyntaxException if the Location URI syntax is incorrect
*/
@PostMapping("faqs")
@EnforceAtLeastInstructor
public ResponseEntity<Faq> createFaq(@RequestBody Faq faq) 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");
}
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null);

Faq savedFaq = faqService.save(faq);
cremertim marked this conversation as resolved.
Show resolved Hide resolved
return ResponseEntity.created(new URI("/api/faqs/" + savedFaq.getId())).body(savedFaq);
}

/**
* PUT /faqs/{faqId} : Updates an existing faq.
*
* @param faq the faq to update
* @param faqId id of the faq to be updated
* @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 with status 500 (Internal
* Server Error) if the faq couldn't be updated
*/
@PutMapping("faqs/{faqId}")
@EnforceAtLeastInstructor
public ResponseEntity<Faq> updateFaq(@RequestBody Faq faq, @PathVariable Long faqId) {
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);
cremertim marked this conversation as resolved.
Show resolved Hide resolved
Faq existingFaq = faqService.findById(faqId).orElseThrow(() -> new EntityNotFoundException("FAQ not found", faqId));
cremertim marked this conversation as resolved.
Show resolved Hide resolved
Faq result = faqService.save(faq);
cremertim marked this conversation as resolved.
Show resolved Hide resolved
return ResponseEntity.ok().body(result);
}

/**
* GET /faqs/:faqId : get the "faqId" faq.
*
* @param faqId the faqId of the faq to retrieve
* @return the ResponseEntity with status 200 (OK) and with body the faq, or with status 404 (Not Found)
*/
@GetMapping("faqs/{faqId}")
@EnforceAtLeastStudent
public ResponseEntity<Faq> getFaq(@PathVariable Long faqId) {
log.debug("REST request to get faq {}", faqId);
Faq faq = faqService.findById(faqId).orElseThrow(() -> new EntityNotFoundException("FAQ not found", faqId));
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, faq.getCourse(), null);
return ResponseEntity.ok(faq);
}

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

log.debug("REST request to delete faq {}", faqId);
Faq faq = faqService.findById(faqId).orElseThrow(() -> new EntityNotFoundException("FAQ not found", faqId));
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null);
faqService.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<Faq>> getFaqForCourse(@PathVariable Long courseId) {
log.debug("REST request to get all Faqs for the course with id : {}", courseId);

Course course = getCourseForRequest(courseId);
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null);
Set<Faq> faqs = faqService.findAllByCourseId(courseId);
return ResponseEntity.ok().body(faqs);
}

/**
* GET /courses/:courseId/faqs : 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
*/
Comment on lines +152 to +157
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct the Javadoc to match the actual endpoint

The Javadoc for getFaqCategoriesForCourse incorrectly specifies the endpoint as GET /courses/:courseId/faqs. It should be GET /courses/:courseId/faq-categories to accurately reflect the mapped URL and the method's functionality.

Apply this diff to fix the Javadoc:

 /**
- * GET /courses/:courseId/faqs : get all the faq categories of a course
+ * 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 FAQ categories in the body
  */
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* GET /courses/:courseId/faqs : 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
*/
/**
* 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 FAQ categories in the 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 = getCourseForRequest(courseId);
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null);
Set<String> faqs = faqService.findAllCategoriesByCourseId(courseId);

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

/**
*
* @param courseId the courseId of the course
* @return the course with the id courseId, unless it exists
*/
cremertim marked this conversation as resolved.
Show resolved Hide resolved
private Course getCourseForRequest(Long courseId) {
return courseRepository.findByIdElseThrow(courseId);
}

}
Loading
Loading