From b3bf2bb468a8c244d3a8504dc3bbb5996fcff4ac Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 3 Sep 2024 16:05:17 +0200 Subject: [PATCH 01/68] FAQ - system provisional backend --- .../de/tum/in/www1/artemis/domain/Course.java | 30 +++- .../de/tum/in/www1/artemis/domain/Faq.java | 82 ++++++++++ .../artemis/repository/FaqRepository.java | 29 ++++ .../in/www1/artemis/service/FaqService.java | 24 +++ .../in/www1/artemis/web/rest/FaqResource.java | 154 ++++++++++++++++++ .../changelog/20240902132940_changelog.xml | 49 ++++++ .../resources/config/liquibase/master.xml | 1 + 7 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/domain/Faq.java create mode 100644 src/main/java/de/tum/in/www1/artemis/repository/FaqRepository.java create mode 100644 src/main/java/de/tum/in/www1/artemis/service/FaqService.java create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java create mode 100644 src/main/resources/config/liquibase/changelog/20240902132940_changelog.xml diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Course.java b/src/main/java/de/tum/in/www1/artemis/domain/Course.java index 128a897777e1..a3ce647be531 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Course.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Course.java @@ -186,6 +186,9 @@ public class Course extends DomainObject { @Column(name = "unenrollment_enabled") private boolean unenrollmentEnabled = false; + @Column(name = "faq_enabled") + private boolean faqEnabled = false; + @Column(name = "presentation_score") private Integer presentationScore; @@ -259,6 +262,10 @@ public class Course extends DomainObject { @JsonIgnoreProperties("course") private TutorialGroupsConfiguration tutorialGroupsConfiguration; + @OneToMany(mappedBy = "course", fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = "course", allowSetters = true) + private Set faqs = new HashSet<>(); + // NOTE: Helpers variable names must be different from Getter name, so that Jackson ignores the @Transient annotation, but Hibernate still respects it @Transient private Long numberOfInstructorsTransient; @@ -626,6 +633,14 @@ public void setEnrollmentEnabled(Boolean enrollmentEnabled) { this.enrollmentEnabled = enrollmentEnabled; } + public Boolean isFaqEnabled() { + return faqEnabled; + } + + public void setFaqEnabled(Boolean faqEnabled) { + this.faqEnabled = faqEnabled; + } + public String getEnrollmentConfirmationMessage() { return enrollmentConfirmationMessage; } @@ -716,7 +731,7 @@ public String toString() { + "'" + ", enrollmentStartDate='" + getEnrollmentStartDate() + "'" + ", enrollmentEndDate='" + getEnrollmentEndDate() + "'" + ", unenrollmentEndDate='" + getUnenrollmentEndDate() + "'" + ", semester='" + getSemester() + "'" + "'" + ", onlineCourse='" + isOnlineCourse() + "'" + ", color='" + getColor() + "'" + ", courseIcon='" + getCourseIcon() + "'" + ", enrollmentEnabled='" + isEnrollmentEnabled() + "'" + ", unenrollmentEnabled='" + isUnenrollmentEnabled() + "'" - + ", presentationScore='" + getPresentationScore() + "'" + "}"; + + ", presentationScore='" + getPresentationScore() + ", faqEnabled='" + isFaqEnabled() + "'" + "}"; } public void setNumberOfInstructors(Long numberOfInstructors) { @@ -1056,4 +1071,17 @@ public String getMappedColumnName() { return mappedColumnName; } } + + public Set getFaqs() { + return faqs; + } + + public void setFaqs(Set faqs) { + this.faqs = faqs; + } + + public void addFaq(Faq faq) { + this.faqs.add(faq); + faq.setCourse(this); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Faq.java b/src/main/java/de/tum/in/www1/artemis/domain/Faq.java new file mode 100644 index 000000000000..fd6225523e08 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/Faq.java @@ -0,0 +1,82 @@ +package de.tum.in.www1.artemis.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.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; + +/** + * A FAQ. + */ +@Entity +@Table(name = "faq") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class Faq extends DomainObject { + + @Column(name = "question_title") + private String questionTitle; + + @Column(name = "question_answer") + private String questionAnswer; + + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "faq_categories", joinColumns = @JoinColumn(name = "faq_id")) + @Column(name = "categories") + private Set categories = new HashSet<>(); + + @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 getCategories() { + return categories; + } + + public void setCategories(Set categories) { + this.categories = categories; + } + + @Override + public String toString() { + return "Faq{" + "id=" + getId() + ", title='" + getQuestionTitle() + "'" + ", description='" + getQuestionTitle() + "'" + "}"; + } + +} diff --git a/src/main/java/de/tum/in/www1/artemis/repository/FaqRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/FaqRepository.java new file mode 100644 index 000000000000..84bbb7ff50da --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/repository/FaqRepository.java @@ -0,0 +1,29 @@ +package de.tum.in.www1.artemis.repository; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; + +import java.util.Set; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import de.tum.in.www1.artemis.domain.Faq; +import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; + +/** + * Spring Data repository for the Faq entity. + */ +@Profile(PROFILE_CORE) +@Repository +public interface FaqRepository extends ArtemisJpaRepository { + + @Query(""" + SELECT faq + FROM Faq faq + WHERE faq.course.id = :courseId + """) + Set findAllByCourseId(@Param("courseId") Long courseId); + +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/FaqService.java b/src/main/java/de/tum/in/www1/artemis/service/FaqService.java new file mode 100644 index 000000000000..d1e762fcf679 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/FaqService.java @@ -0,0 +1,24 @@ +package de.tum.in.www1.artemis.service; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +@Profile(PROFILE_CORE) +@Service +public class FaqService { + + public FaqService() { + + } + + /** + * Deletes the given lecture (with its lecture units). + * + * @param faqId the faqId of to be deleted faq + */ + public void delete(long faqId) { + } + +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java new file mode 100644 index 000000000000..f52d0e0f8051 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java @@ -0,0 +1,154 @@ +package de.tum.in.www1.artemis.web.rest; + +import static de.tum.in.www1.artemis.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.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.Faq; +import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.repository.FaqRepository; +import de.tum.in.www1.artemis.security.Role; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastEditor; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; +import de.tum.in.www1.artemis.service.AuthorizationCheckService; +import de.tum.in.www1.artemis.service.FaqService; +import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; +import de.tum.in.www1.artemis.web.rest.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 FaqRepository faqRepository; + + private final FaqService faqService; + + private final CourseRepository courseRepository; + + private final AuthorizationCheckService authCheckService; + + public FaqResource(FaqRepository faqRepository, FaqService faqService, CourseRepository courseRepository, AuthorizationCheckService authCheckService) { + + this.faqRepository = faqRepository; + 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") + @EnforceAtLeastEditor + public ResponseEntity 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"); + } + System.out.println("Test"); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, faq.getCourse(), null); + + Faq savedFaq = faqRepository.save(faq); + return ResponseEntity.created(new URI("/api/faqs/" + savedFaq.getId())).body(savedFaq); + } + + /** + * PUT /faqs/{faqId} : Updates an existing faq. + * + * @param faq the faq to update + * @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") + @EnforceAtLeastEditor + public ResponseEntity updateFaq(@RequestBody Faq faq) { + log.debug("REST request to update Faq : {}", faq); + if (faq.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idNull"); + } + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, faq.getCourse(), null); + Faq result = faqRepository.save(faq); + return ResponseEntity.ok().body(result); + } + + /** + * 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") + @EnforceAtLeastEditor + public ResponseEntity> 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.EDITOR, course, null); + + Set faqs = faqRepository.findAllByCourseId(courseId); + + return ResponseEntity.ok().body(faqs); + } + + /** + * 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 getFaq(@PathVariable Long faqId) { + log.debug("REST request to get faq {}", faqId); + Faq faq = faqRepository.findById(faqId).orElseThrow(); + + 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 deleteFaq(@PathVariable Long faqId) { + log.debug("REST request to delete faq {}", faqId); + faqService.delete(faqId); + return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, faqId.toString())).build(); + } +} diff --git a/src/main/resources/config/liquibase/changelog/20240902132940_changelog.xml b/src/main/resources/config/liquibase/changelog/20240902132940_changelog.xml new file mode 100644 index 000000000000..8ea56581c20e --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240902132940_changelog.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index c5be79948d2f..212ac2b59b24 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -21,6 +21,7 @@ + From f49731e0e3b1f8aa8ed4753beb4df6c37ef636d8 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 3 Sep 2024 17:52:41 +0200 Subject: [PATCH 02/68] Add meta information and state to FAQ --- .../de/tum/in/www1/artemis/domain/Faq.java | 18 ++++++++++++++++-- .../tum/in/www1/artemis/domain/FaqState.java | 5 +++++ .../in/www1/artemis/web/rest/FaqResource.java | 2 +- ...ngelog.xml => 20240902175045_changelog.xml} | 15 ++++++++++++--- src/main/resources/config/liquibase/master.xml | 2 +- 5 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/domain/FaqState.java rename src/main/resources/config/liquibase/changelog/{20240902132940_changelog.xml => 20240902175045_changelog.xml} (78%) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Faq.java b/src/main/java/de/tum/in/www1/artemis/domain/Faq.java index fd6225523e08..98746c97fab9 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Faq.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Faq.java @@ -7,6 +7,8 @@ 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; @@ -25,7 +27,7 @@ @Table(name = "faq") @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @JsonInclude(JsonInclude.Include.NON_EMPTY) -public class Faq extends DomainObject { +public class Faq extends AbstractAuditingEntity { @Column(name = "question_title") private String questionTitle; @@ -38,6 +40,10 @@ public class Faq extends DomainObject { @Column(name = "categories") private Set categories = new HashSet<>(); + @Enumerated(EnumType.STRING) + @Column(name = "faq_state") + private FaqState faqState; + @ManyToOne @JsonIgnoreProperties(value = { "faqs" }, allowSetters = true) private Course course; @@ -74,9 +80,17 @@ public void setCategories(Set 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() + "'" + "}"; + return "Faq{" + "id=" + getId() + ", title='" + getQuestionTitle() + "'" + ", description='" + getQuestionTitle() + "'" + ", faqState='" + getFaqState() + "}"; } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/FaqState.java b/src/main/java/de/tum/in/www1/artemis/domain/FaqState.java new file mode 100644 index 000000000000..7ba46b7dddb5 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/FaqState.java @@ -0,0 +1,5 @@ +package de.tum.in.www1.artemis.domain; + +public enum FaqState { + ACCEPTED, REJECTED, PROPOSED +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java index f52d0e0f8051..0b779164e0fa 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java @@ -92,7 +92,7 @@ public ResponseEntity createFaq(@RequestBody Faq faq) throws URISyntaxExcep * @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") + @PutMapping("faqs/{faqId}") @EnforceAtLeastEditor public ResponseEntity updateFaq(@RequestBody Faq faq) { log.debug("REST request to update Faq : {}", faq); diff --git a/src/main/resources/config/liquibase/changelog/20240902132940_changelog.xml b/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml similarity index 78% rename from src/main/resources/config/liquibase/changelog/20240902132940_changelog.xml rename to src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml index 8ea56581c20e..56c360204fc2 100644 --- a/src/main/resources/config/liquibase/changelog/20240902132940_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml @@ -7,21 +7,30 @@ - + - + + + + + + + + + + - + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 212ac2b59b24..b2dd2e527953 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -21,7 +21,7 @@ - + From c2efdbbb465c9ede9ec8166958abe3449fc3a2e8 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 3 Sep 2024 18:08:07 +0200 Subject: [PATCH 03/68] Fixed minor mapping error --- src/main/java/de/tum/in/www1/artemis/domain/Faq.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Faq.java b/src/main/java/de/tum/in/www1/artemis/domain/Faq.java index 98746c97fab9..8b1e12287d96 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Faq.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Faq.java @@ -37,7 +37,7 @@ public class Faq extends AbstractAuditingEntity { @ElementCollection(fetch = FetchType.LAZY) @CollectionTable(name = "faq_categories", joinColumns = @JoinColumn(name = "faq_id")) - @Column(name = "categories") + @Column(name = "category") private Set categories = new HashSet<>(); @Enumerated(EnumType.STRING) From 5467a1039330e0cf08cb40eb352e6a2e6d1efbdc Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 5 Sep 2024 15:05:53 +0200 Subject: [PATCH 04/68] Added cascade delete --- src/main/java/de/tum/in/www1/artemis/domain/Course.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Course.java b/src/main/java/de/tum/in/www1/artemis/domain/Course.java index a3ce647be531..8d3ae69c54a2 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Course.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Course.java @@ -262,7 +262,7 @@ public class Course extends DomainObject { @JsonIgnoreProperties("course") private TutorialGroupsConfiguration tutorialGroupsConfiguration; - @OneToMany(mappedBy = "course", fetch = FetchType.LAZY) + @OneToMany(mappedBy = "course", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) @JsonIgnoreProperties(value = "course", allowSetters = true) private Set faqs = new HashSet<>(); From ebe804473d1aa807b1a77cdf5b042a31a6a09938 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Mon, 9 Sep 2024 13:09:43 +0200 Subject: [PATCH 05/68] Added cascade deletion on course deletion --- .../de/tum/in/www1/artemis/domain/Faq.java | 4 +- .../artemis/repository/FaqRepository.java | 18 ++++++ .../www1/artemis/service/CourseService.java | 11 +++- .../in/www1/artemis/service/FaqService.java | 10 +++- .../in/www1/artemis/web/rest/FaqResource.java | 58 ++++++++++++------- 5 files changed, 74 insertions(+), 27 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Faq.java b/src/main/java/de/tum/in/www1/artemis/domain/Faq.java index 8b1e12287d96..61bb6f525526 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Faq.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Faq.java @@ -35,9 +35,9 @@ public class Faq extends AbstractAuditingEntity { @Column(name = "question_answer") private String questionAnswer; - @ElementCollection(fetch = FetchType.LAZY) + @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "faq_categories", joinColumns = @JoinColumn(name = "faq_id")) - @Column(name = "category") + @Column(name = "categories") private Set categories = new HashSet<>(); @Enumerated(EnumType.STRING) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/FaqRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/FaqRepository.java index 84bbb7ff50da..8e04d87fdb42 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/FaqRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/FaqRepository.java @@ -5,9 +5,11 @@ 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.in.www1.artemis.domain.Faq; import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; @@ -26,4 +28,20 @@ public interface FaqRepository extends ArtemisJpaRepository { """) Set findAllByCourseId(@Param("courseId") Long courseId); + @Query(""" + SELECT distinct faq.categories + FROM Faq faq + WHERE faq.course.id = :courseId + """) + Set findAllCategoriesByCourseId(@Param("courseId") Long courseId); + + @Transactional + @Modifying + @Query(""" + DELETE + FROM Faq faq + WHERE faq.course.id = :courseId + """) + void deleteAllByCourseId(@Param("courseId") Long courseId); + } diff --git a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java index 8015980acfe5..a8395bb2c486 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java @@ -66,6 +66,7 @@ import de.tum.in.www1.artemis.repository.ExamRepository; import de.tum.in.www1.artemis.repository.ExerciseGroupRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; +import de.tum.in.www1.artemis.repository.FaqRepository; import de.tum.in.www1.artemis.repository.GradingScaleRepository; import de.tum.in.www1.artemis.repository.GroupNotificationRepository; import de.tum.in.www1.artemis.repository.LectureRepository; @@ -111,6 +112,8 @@ public class CourseService { private static final Logger log = LoggerFactory.getLogger(CourseService.class); + private final FaqRepository faqRepository; + @Value("${artemis.course-archives-path}") private Path courseArchivesDirPath; @@ -204,7 +207,7 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise TutorialGroupRepository tutorialGroupRepository, PlagiarismCaseRepository plagiarismCaseRepository, ConversationRepository conversationRepository, LearningPathService learningPathService, Optional irisSettingsService, LectureRepository lectureRepository, TutorialGroupNotificationRepository tutorialGroupNotificationRepository, TutorialGroupChannelManagementService tutorialGroupChannelManagementService, - PrerequisiteRepository prerequisiteRepository, CompetencyRelationRepository competencyRelationRepository) { + PrerequisiteRepository prerequisiteRepository, CompetencyRelationRepository competencyRelationRepository, FaqRepository faqRepository) { this.courseRepository = courseRepository; this.exerciseService = exerciseService; this.exerciseDeletionService = exerciseDeletionService; @@ -244,6 +247,7 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise this.tutorialGroupChannelManagementService = tutorialGroupChannelManagementService; this.prerequisiteRepository = prerequisiteRepository; this.competencyRelationRepository = competencyRelationRepository; + this.faqRepository = faqRepository; } /** @@ -461,6 +465,7 @@ public void delete(Course course) { deleteDefaultGroups(course); deleteExamsOfCourse(course); deleteGradingScaleOfCourse(course); + deleteFaqOfCourse(course); irisSettingsService.ifPresent(iss -> iss.deleteSettingsFor(course)); courseRepository.deleteById(course.getId()); log.debug("Successfully deleted course {}.", course.getTitle()); @@ -536,6 +541,10 @@ private void deleteCompetenciesOfCourse(Course course) { competencyRepository.deleteAll(course.getCompetencies()); } + private void deleteFaqOfCourse(Course course) { + faqRepository.deleteAllByCourseId(course.getId()); + } + /** * If the exercise is part of an exam, retrieve the course through ExerciseGroup -> Exam -> Course. * Otherwise, the course is already set and the id can be used to retrieve the course from the database. diff --git a/src/main/java/de/tum/in/www1/artemis/service/FaqService.java b/src/main/java/de/tum/in/www1/artemis/service/FaqService.java index d1e762fcf679..0f213a1fc8e2 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/FaqService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/FaqService.java @@ -5,12 +5,16 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import de.tum.in.www1.artemis.repository.FaqRepository; + @Profile(PROFILE_CORE) @Service public class FaqService { - public FaqService() { + private final FaqRepository faqRepository; + public FaqService(FaqRepository faqRepository) { + this.faqRepository = faqRepository; } /** @@ -18,7 +22,9 @@ public FaqService() { * * @param faqId the faqId of to be deleted faq */ - public void delete(long faqId) { + public void deleteById(long faqId) { + faqRepository.deleteById(faqId); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java index 0b779164e0fa..21ad760776cd 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java @@ -78,7 +78,6 @@ public ResponseEntity createFaq(@RequestBody Faq faq) throws URISyntaxExcep if (faq.getId() != null) { throw new BadRequestAlertException("A new faq cannot already have an ID", ENTITY_NAME, "idExists"); } - System.out.println("Test"); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, faq.getCourse(), null); Faq savedFaq = faqRepository.save(faq); @@ -104,25 +103,6 @@ public ResponseEntity updateFaq(@RequestBody Faq faq) { return ResponseEntity.ok().body(result); } - /** - * 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") - @EnforceAtLeastEditor - public ResponseEntity> 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.EDITOR, course, null); - - Set faqs = faqRepository.findAllByCourseId(courseId); - - return ResponseEntity.ok().body(faqs); - } - /** * GET /faqs/:faqId : get the "faqId" faq. * @@ -134,7 +114,7 @@ public ResponseEntity> getFaqForCourse(@PathVariable Long courseId) { public ResponseEntity getFaq(@PathVariable Long faqId) { log.debug("REST request to get faq {}", faqId); Faq faq = faqRepository.findById(faqId).orElseThrow(); - + System.out.println(faq.getCategories()); return ResponseEntity.ok(faq); } @@ -147,8 +127,42 @@ public ResponseEntity getFaq(@PathVariable Long faqId) { @DeleteMapping("faqs/{faqId}") @EnforceAtLeastInstructor public ResponseEntity deleteFaq(@PathVariable Long faqId) { + log.debug("REST request to delete faq {}", faqId); - faqService.delete(faqId); + 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") + @EnforceAtLeastEditor + public ResponseEntity> 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.EDITOR, course, null); + + Set faqs = faqRepository.findAllByCourseId(courseId); + return ResponseEntity.ok().body(faqs); + } + + @GetMapping("courses/{courseId}/faq-categories") + @EnforceAtLeastEditor + public ResponseEntity> getFaqCategoriesForCourse(@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.EDITOR, course, null); + + Set faqs = faqRepository.findAllCategoriesByCourseId(courseId); + + return ResponseEntity.ok().body(faqs); + } + } From 2d3ea4e550ce355661910aac69b943df6766a531 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 10 Sep 2024 10:35:51 +0200 Subject: [PATCH 06/68] Changed rest of server stuff --- src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java | 1 - .../config/liquibase/changelog/20240902175045_changelog.xml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java index 21ad760776cd..c9b961348ca1 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java @@ -114,7 +114,6 @@ public ResponseEntity updateFaq(@RequestBody Faq faq) { public ResponseEntity getFaq(@PathVariable Long faqId) { log.debug("REST request to get faq {}", faqId); Faq faq = faqRepository.findById(faqId).orElseThrow(); - System.out.println(faq.getCategories()); return ResponseEntity.ok(faq); } diff --git a/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml b/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml index 56c360204fc2..3f2400598da3 100644 --- a/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml @@ -30,7 +30,7 @@ - + From be22131dcd69029262badaebd647e4f86d865c5a Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 10 Sep 2024 11:37:31 +0200 Subject: [PATCH 07/68] Add translations and fix uppercase --- .../artemis/repository/FaqRepository.java | 2 +- src/main/webapp/i18n/de/course.json | 4 +++ src/main/webapp/i18n/de/faq.json | 26 +++++++++++++++++++ src/main/webapp/i18n/de/global.json | 3 ++- src/main/webapp/i18n/en/course.json | 4 +++ src/main/webapp/i18n/en/faq.json | 26 +++++++++++++++++++ src/main/webapp/i18n/en/global.json | 3 ++- 7 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 src/main/webapp/i18n/de/faq.json create mode 100644 src/main/webapp/i18n/en/faq.json diff --git a/src/main/java/de/tum/in/www1/artemis/repository/FaqRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/FaqRepository.java index 8e04d87fdb42..dd36a4940187 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/FaqRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/FaqRepository.java @@ -29,7 +29,7 @@ public interface FaqRepository extends ArtemisJpaRepository { Set findAllByCourseId(@Param("courseId") Long courseId); @Query(""" - SELECT distinct faq.categories + SELECT DISTINCT faq.categories FROM Faq faq WHERE faq.course.id = :courseId """) diff --git a/src/main/webapp/i18n/de/course.json b/src/main/webapp/i18n/de/course.json index 7bc9274e7461..cdcf9ffdd322 100644 --- a/src/main/webapp/i18n/de/course.json +++ b/src/main/webapp/i18n/de/course.json @@ -106,6 +106,10 @@ "label": "Direktnachrichten / Gruppen-Chats aktiviert", "tooltip": "Ermöglicht den Nachrichtenaustausch in Gruppenchats oder Direktnachrichten. Alle Nutzer:innen können Direktnachrichten oder einen privaten Gruppenchat starten und andere Nutzer:innen hinzufügen. Ein Gruppenchat ist auf zehn Mitglieder:innen begrenzt. Die Chats finden im Kommunikationbereich des Kurses statt.", "codeOfConduct": "Nachrichten: Code of Conduct" + }, + "faqEnabled": { + "label": "FAQ aktivieren", + "tooltip": "Ermöglicht das Erstellen von FAQ-Einträgen, in denen Lehrende häufig gestellte Fragen sammeln. Studierende können auf diese Wissenssammlung zugreifen, um eigenständig nachzuarbeiten und ihre Fragen zu klären." } }, "enrollmentEnabled": { diff --git a/src/main/webapp/i18n/de/faq.json b/src/main/webapp/i18n/de/faq.json new file mode 100644 index 000000000000..585a84a147d3 --- /dev/null +++ b/src/main/webapp/i18n/de/faq.json @@ -0,0 +1,26 @@ +{ + "artemisApp": { + "faq": { + "home": { + "title": "FAQ", + "createLabel": "FAQ erstellen", + "filterLabel": "Filter", + "createOrEditLabel": "FAQ erstellen oder bearbeiten" + }, + "created": "FAQ erstellt mit ID {{ param }}", + "updated": "FAQ aktualisiert mit ID {{ param }}", + "deleted": "FAQ gelöscht mit ID {{ param }}", + "delete": { + "question": "Soll das FAQ {{ title }} wirklich dauerhaft gelöscht werden? Diese Aktion kann NICHT rückgängig gemacht werden!", + "typeNameToConfirm": "Bitte gib den Namen des FAQ zur Bestätigung ein." + }, + + "table": { + "questionTitle": "Fragentitel", + "questionAnswer": "Antwort auf die Frage", + "categories": "Kategorien" + }, + "course": "Kurs" + } + } +} diff --git a/src/main/webapp/i18n/de/global.json b/src/main/webapp/i18n/de/global.json index 9db7bb972f26..31fabe6283de 100644 --- a/src/main/webapp/i18n/de/global.json +++ b/src/main/webapp/i18n/de/global.json @@ -266,7 +266,8 @@ "goBack": "Zurück", "search": "Suchen", "select": "Auswählen", - "sendToIris": "An Iris schicken" + "sendToIris": "An Iris schicken", + "faq": "FAQ" }, "detail": { "field": "Feld", diff --git a/src/main/webapp/i18n/en/course.json b/src/main/webapp/i18n/en/course.json index e977ca0d3f3f..8fee816f7faf 100644 --- a/src/main/webapp/i18n/en/course.json +++ b/src/main/webapp/i18n/en/course.json @@ -106,6 +106,10 @@ "label": "Direct Messages / Group Chats Enabled", "tooltip": "Enables messaging between course users in group chats or direct messages. Every user can start a direct message, private group chat and add other users. A group chat is limited to 10 members. The chats happens in the communication space of the course.", "codeOfConduct": "Messaging Code of Conduct" + }, + "faqEnabled": { + "label": "FAQ Enabled", + "tooltip": "Enables the creation of FAQ entries where instructors can collect frequently asked questions. Students can access this knowledge base to review independently and clarify their questions." } }, "enrollmentEnabled": { diff --git a/src/main/webapp/i18n/en/faq.json b/src/main/webapp/i18n/en/faq.json new file mode 100644 index 000000000000..26567d8c3927 --- /dev/null +++ b/src/main/webapp/i18n/en/faq.json @@ -0,0 +1,26 @@ +{ + "artemisApp": { + "faq": { + "home": { + "title": "FAQ", + "createLabel": "Create a new FAQ", + "filterLabel": "Filter", + "createOrEditLabel": "FAQ erstellen oder bearbeiten" + }, + "created": "Created new FAQ with identifier {{ param }}", + "updated": "Updated FAQ with identifier {{ param }}", + "deleted": "Deleted FAQ with identifier {{ param }}", + "delete": { + "question": "Are you sure you want to permanently delete the FAQ {{ title }}? This action can NOT be undone!", + "typeNameToConfirm": "Please type in the name of the FAQ to confirm." + }, + + "table": { + "questionTitle": "Question title", + "questionAnswer": "Question answer", + "categories": "Categories" + }, + "course": "Course" + } + } +} diff --git a/src/main/webapp/i18n/en/global.json b/src/main/webapp/i18n/en/global.json index 8760c0192b62..18854a2515a7 100644 --- a/src/main/webapp/i18n/en/global.json +++ b/src/main/webapp/i18n/en/global.json @@ -268,7 +268,8 @@ "goBack": "Go back", "search": "Search", "select": "Select", - "sendToIris": "Send To Iris" + "sendToIris": "Send To Iris", + "faq": "FAQ" }, "detail": { "field": "Field", From 407072efcc6f6773dcdd234735bf45cab1b49134 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 10 Sep 2024 19:49:22 +0200 Subject: [PATCH 08/68] Add first draft of FAQ System --- package-lock.json | 86 +++++----- .../course-management-tab-bar.component.html | 6 + .../course-management-tab-bar.component.ts | 2 + .../course/manage/course-management.module.ts | 2 + .../manage/course-update.component.html | 20 +++ .../course/manage/course-update.component.ts | 8 +- .../course-management-card.component.html | 12 ++ .../course-management-card.component.ts | 2 + src/main/webapp/app/entities/course.model.ts | 1 + .../webapp/app/entities/faq-category.model.ts | 29 ++++ src/main/webapp/app/entities/faq.model.ts | 24 +++ .../webapp/app/faq/faq-update.component.html | 52 ++++++ .../webapp/app/faq/faq-update.component.scss | 7 + .../webapp/app/faq/faq-update.component.ts | 154 ++++++++++++++++++ src/main/webapp/app/faq/faq.component.html | 123 ++++++++++++++ src/main/webapp/app/faq/faq.component.ts | 144 ++++++++++++++++ src/main/webapp/app/faq/faq.module.ts | 34 ++++ src/main/webapp/app/faq/faq.routes.ts | 89 ++++++++++ src/main/webapp/app/faq/faq.service.ts | 149 +++++++++++++++++ src/main/webapp/app/faq/faq.utils.ts | 33 ++++ .../category-selector.component.ts | 5 +- ...ustom-exercise-category-badge.component.ts | 3 +- 22 files changed, 938 insertions(+), 47 deletions(-) create mode 100644 src/main/webapp/app/entities/faq-category.model.ts create mode 100644 src/main/webapp/app/entities/faq.model.ts create mode 100644 src/main/webapp/app/faq/faq-update.component.html create mode 100644 src/main/webapp/app/faq/faq-update.component.scss create mode 100644 src/main/webapp/app/faq/faq-update.component.ts create mode 100644 src/main/webapp/app/faq/faq.component.html create mode 100644 src/main/webapp/app/faq/faq.component.ts create mode 100644 src/main/webapp/app/faq/faq.module.ts create mode 100644 src/main/webapp/app/faq/faq.routes.ts create mode 100644 src/main/webapp/app/faq/faq.service.ts create mode 100644 src/main/webapp/app/faq/faq.utils.ts diff --git a/package-lock.json b/package-lock.json index f05b86d4cc8f..08cc6a211342 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3792,7 +3792,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -3810,7 +3810,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -3823,7 +3823,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -3836,14 +3836,14 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -3861,7 +3861,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -3877,7 +3877,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -8016,7 +8016,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -8698,7 +8698,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/connect-history-api-fallback": { @@ -9095,7 +9095,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -9911,7 +9911,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/ee-first": { @@ -11405,7 +11405,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -11497,7 +11497,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -11681,7 +11681,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -11721,7 +11721,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -11732,7 +11732,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -12384,7 +12384,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -12710,7 +12710,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/ismobilejs-es5": { @@ -16125,7 +16125,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -17297,7 +17297,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true, + "devOptional": true, "license": "BlueOak-1.0.0" }, "node_modules/pacote": { @@ -17458,7 +17458,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -17468,7 +17468,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -18657,7 +18657,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "glob": "^11.0.0", @@ -18677,7 +18677,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -18701,7 +18701,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", - "dev": true, + "devOptional": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -18720,7 +18720,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": "20 || >=22" @@ -18730,7 +18730,7 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -18746,7 +18746,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, + "devOptional": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -19219,7 +19219,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -19232,7 +19232,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -19322,7 +19322,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">=14" @@ -19802,7 +19802,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -19817,14 +19817,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -19876,7 +19876,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -21971,7 +21971,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -22062,7 +22062,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -22080,7 +22080,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -22096,7 +22096,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -22109,21 +22109,21 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -22133,7 +22133,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", diff --git a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html index 3c953bcf4e3a..abbfdaf7c010 100644 --- a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html +++ b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html @@ -72,6 +72,12 @@ } + @if (course.isAtLeastInstructor && course.faqEnabled) { + + + + + } @if (course.isAtLeastInstructor && localCIActive) { diff --git a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts index c46c04bbcf5d..bf4cf8452999 100644 --- a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts +++ b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts @@ -25,6 +25,7 @@ import { faTrash, faUserCheck, faWrench, + faQuestion } from '@fortawesome/free-solid-svg-icons'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; import { CourseAdminService } from 'app/course/manage/course-admin.service'; @@ -73,6 +74,7 @@ export class CourseManagementTabBarComponent implements OnInit, OnDestroy, After faRobot = faRobot; faPuzzlePiece = faPuzzlePiece; faList = faList; + faQuestion = faQuestion isCommunicationEnabled = false; diff --git a/src/main/webapp/app/course/manage/course-management.module.ts b/src/main/webapp/app/course/manage/course-management.module.ts index 333de23de8b3..22cfc65562a8 100644 --- a/src/main/webapp/app/course/manage/course-management.module.ts +++ b/src/main/webapp/app/course/manage/course-management.module.ts @@ -70,6 +70,7 @@ import { SubmissionResultStatusModule } from 'app/overview/submission-result-sta import { ImageCropperModalComponent } from 'app/course/manage/image-cropper-modal.component'; import { HeaderCourseComponent } from 'app/overview/header-course.component'; import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; +import { ArtemisFAQModule } from 'app/faq/faq.module'; @NgModule({ imports: [ @@ -124,6 +125,7 @@ import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown DetailModule, SubmissionResultStatusModule, ArtemisMarkdownEditorModule, + ArtemisFAQModule, ], declarations: [ CourseManagementComponent, diff --git a/src/main/webapp/app/course/manage/course-update.component.html b/src/main/webapp/app/course/manage/course-update.component.html index 43b03f3f9040..4906832adf6c 100644 --- a/src/main/webapp/app/course/manage/course-update.component.html +++ b/src/main/webapp/app/course/manage/course-update.component.html @@ -347,6 +347,26 @@
ngbTooltip="{{ 'artemisApp.course.courseCommunicationSetting.messagingEnabled.tooltip' | artemisTranslate }}" /> +
+ + + +
@if (communicationEnabled) {
diff --git a/src/main/webapp/app/course/manage/course-update.component.ts b/src/main/webapp/app/course/manage/course-update.component.ts index a3406ad5f46b..54227ed00aed 100644 --- a/src/main/webapp/app/course/manage/course-update.component.ts +++ b/src/main/webapp/app/course/manage/course-update.component.ts @@ -71,6 +71,7 @@ export class CourseUpdateComponent implements OnInit { faExclamationTriangle = faExclamationTriangle; faPen = faPen; + faqEnabled = true communicationEnabled = true; messagingEnabled = true; ltiEnabled = false; @@ -115,7 +116,7 @@ export class CourseUpdateComponent implements OnInit { this.courseOrganizations = organizations; }); this.originalTimeZone = this.course.timeZone; - + this.faqEnabled = course.faqEnabled // complaints are only enabled when at least one complaint is allowed and the complaint duration is positive this.complaintsEnabled = (this.course.maxComplaints! > 0 || this.course.maxTeamComplaints! > 0) && @@ -295,10 +296,13 @@ export class CourseUpdateComponent implements OnInit { if (this.communicationEnabled && this.messagingEnabled) { course['courseInformationSharingConfiguration'] = CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING; + course.faqEnabled = this.faqEnabled } else if (this.communicationEnabled && !this.messagingEnabled) { course['courseInformationSharingConfiguration'] = CourseInformationSharingConfiguration.COMMUNICATION_ONLY; + course.faqEnabled = this.faqEnabled } else { this.communicationEnabled = false; + this.faqEnabled = false course['courseInformationSharingConfiguration'] = CourseInformationSharingConfiguration.DISABLED; } @@ -650,7 +654,9 @@ export class CourseUpdateComponent implements OnInit { disableMessaging() { this.messagingEnabled = false; + this.faqEnabled = false } + } const CourseValidator: ValidatorFn = (formGroup: FormGroup) => { diff --git a/src/main/webapp/app/course/manage/overview/course-management-card.component.html b/src/main/webapp/app/course/manage/overview/course-management-card.component.html index adf01ba9af77..d13dee53b6e5 100644 --- a/src/main/webapp/app/course/manage/overview/course-management-card.component.html +++ b/src/main/webapp/app/course/manage/overview/course-management-card.component.html @@ -338,5 +338,17 @@

} + + @if (course.isAtLeastInstructor && course.faqEnabled) { + + + + + }

diff --git a/src/main/webapp/app/course/manage/overview/course-management-card.component.ts b/src/main/webapp/app/course/manage/overview/course-management-card.component.ts index 32dca0a3fc2d..e5fdaec28a2e 100644 --- a/src/main/webapp/app/course/manage/overview/course-management-card.component.ts +++ b/src/main/webapp/app/course/manage/overview/course-management-card.component.ts @@ -22,6 +22,7 @@ import { faSpinner, faTable, faUserCheck, + faQuestion } from '@fortawesome/free-solid-svg-icons'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; @@ -77,6 +78,7 @@ export class CourseManagementCardComponent implements OnChanges { faAngleUp = faAngleUp; faPersonChalkboard = faPersonChalkboard; faSpinner = faSpinner; + faQuestion = faQuestion courseColor: string; diff --git a/src/main/webapp/app/entities/course.model.ts b/src/main/webapp/app/entities/course.model.ts index 4f179de3a687..cd61cefdec33 100644 --- a/src/main/webapp/app/entities/course.model.ts +++ b/src/main/webapp/app/entities/course.model.ts @@ -62,6 +62,7 @@ export class Course implements BaseEntity { public color?: string; public courseIcon?: string; public onlineCourse?: boolean; + public faqEnabled?: boolean public enrollmentEnabled?: boolean; public enrollmentConfirmationMessage?: string; public unenrollmentEnabled?: boolean; diff --git a/src/main/webapp/app/entities/faq-category.model.ts b/src/main/webapp/app/entities/faq-category.model.ts new file mode 100644 index 000000000000..6d62502ac923 --- /dev/null +++ b/src/main/webapp/app/entities/faq-category.model.ts @@ -0,0 +1,29 @@ +export class FaqCategory { + public color?: string; + + public category?: string; + + constructor(category: string | undefined, color: string | undefined) { + this.color = color; + this.category = category; + } + + equals(otherExerciseCategory: FaqCategory): boolean { + return this.color === otherExerciseCategory.color && this.category === otherExerciseCategory.category; + } + + /** + * @param otherExerciseCategory + * @returns the alphanumerical order of the two exercise categories based on their display text + */ + compare(otherExerciseCategory: FaqCategory): number { + if (this.category === otherExerciseCategory.category) { + return 0; + } + + const displayText = this.category?.toLowerCase() ?? ''; + const otherCategoryDisplayText = otherExerciseCategory.category?.toLowerCase() ?? ''; + + return displayText < otherCategoryDisplayText ? -1 : 1; + } +} diff --git a/src/main/webapp/app/entities/faq.model.ts b/src/main/webapp/app/entities/faq.model.ts new file mode 100644 index 000000000000..ea28d55b5c5f --- /dev/null +++ b/src/main/webapp/app/entities/faq.model.ts @@ -0,0 +1,24 @@ +import { BaseEntity } from 'app/shared/model/base-entity'; +import { Course } from 'app/entities/course.model'; +import {FaqCategory} from "app/entities/faq-category.model"; + +export enum FaqState{ + ACCEPTED, REJECTED, PROPOSED +} + +export class Faq implements BaseEntity { + public id?: number; + public questionTitle?: string; + public questionAnswer?: string; + public faqState? : FaqState + public course? : Course + public categories?: FaqCategory[] + + // + isAtLeastEditor?: boolean; + isAtLeastInstructor?: boolean; + + + constructor() { + } +} diff --git a/src/main/webapp/app/faq/faq-update.component.html b/src/main/webapp/app/faq/faq-update.component.html new file mode 100644 index 000000000000..d62dd2535ff7 --- /dev/null +++ b/src/main/webapp/app/faq/faq-update.component.html @@ -0,0 +1,52 @@ +
+
+ @if (true) { +
+
+
+
+

+
+
+
+
+
+ +
+ +
+
+
+ + +
+
+ + + +
+ @if (faq.course) { +
+ +
+ +
+
+ } +
+
+ + +
+
+ +
+ +
+ } +
+
diff --git a/src/main/webapp/app/faq/faq-update.component.scss b/src/main/webapp/app/faq/faq-update.component.scss new file mode 100644 index 000000000000..0e27c3189cd2 --- /dev/null +++ b/src/main/webapp/app/faq/faq-update.component.scss @@ -0,0 +1,7 @@ +.markdown-editor { + height: 350px; +} + + + + diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts new file mode 100644 index 000000000000..a9b796614a0e --- /dev/null +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -0,0 +1,154 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { AlertService } from 'app/core/util/alert.service'; +import { CourseManagementService } from '../course/manage/course-management.service'; +import { Course } from 'app/entities/course.model'; +import { onError } from 'app/shared/util/global.utils'; +import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; +import { faBan, faQuestionCircle, faSave } from '@fortawesome/free-solid-svg-icons'; +import { MonacoFormulaAction } from 'app/shared/monaco-editor/model/actions/monaco-formula.action'; +import { Faq } from 'app/entities/faq.model'; +import { FaqService } from 'app/faq/faq.service'; + +import { FaqCategory } from 'app/entities/faq-category.model'; +import { loadCourseFaqCategories } from 'app/faq/faq.utils'; + +@Component({ + selector: 'jhi-faq-update', + templateUrl: './faq-update.component.html', + styleUrls: ['./faq-update.component.scss'], +}) +export class FAQUpdateComponent implements OnInit { + + faq: Faq; + isSaving: boolean; + existingCategories: FaqCategory[] = [] + exerciseCategories: FaqCategory[] = [] + + courses: Course[]; + + domainActionsDescription = [new MonacoFormulaAction()]; + file: File; + fileName: string; + + // Icons + faQuestionCircle = faQuestionCircle; + faSave = faSave; + faBan = faBan; + + constructor( + protected alertService: AlertService, + protected faqService : FaqService, + protected courseService: CourseManagementService, + protected activatedRoute: ActivatedRoute, + private navigationUtilService: ArtemisNavigationUtilService, + private router: Router, + ) {} + + /** + * Life cycle hook called by Angular to indicate that Angular is done creating the component + */ + ngOnInit() { + this.isSaving = false; + this.activatedRoute.parent!.data.subscribe((data) => { + // Create a new faq to use unless we fetch an existing faq + const faq = data['faq']; + this.faq = faq ?? new Faq(); + const course = data['course']; + if (course) { + this.faq.course = course; + this.loadCourseFaqCategories(course.id) + } + if(faq.categories){ + this.exerciseCategories = faq.categories + } + }); + + } + + /** + * Revert to the previous state, equivalent with pressing the back button on your browser + * Returns to the detail page if there is no previous state and we edited an existing faq + * Returns to the overview page if there is no previous state and we created a new faq + */ + + previousState() { + this.navigationUtilService.navigateBack(['course-management', this.faq.course!.id!.toString(), 'faqs']); + } + /** + * Save the changes on a faq + * This function is called by pressing save after creating or editing a faq + */ + save() { + this.isSaving = true; + if (this.faq.id !== undefined) { + this.subscribeToSaveResponse(this.faqService.update(this.faq)); + } else { + // Newly created faq must have a channel name, which cannot be undefined + console.log(this.faq) + this.subscribeToSaveResponse(this.faqService.create(this.faq)); + + } + } + + /** + * @param result The Http response from the server + */ + protected subscribeToSaveResponse(result: Observable>) { + result.subscribe({ + next: (response: HttpResponse) => this.onSaveSuccess(response.body!), + error: (error: HttpErrorResponse) => this.onSaveError(error), + }); + } + + /** + * Action on successful faq creation or edit + */ + protected onSaveSuccess(faq: Faq) { + if (!this.faq.id) { + this.faqService.find(faq.id!).subscribe({ + next: (response: HttpResponse) => { + this.isSaving = false; + this.faq = response.body!; + this.alertService.success(`FAQ with title ${faq.questionTitle} was successfully created.`); + + }, + }); + } + else { + this.isSaving = false; + this.router.navigate(['course-management', faq.course!.id, 'faqs']); + } + } + + /** + * Action on unsuccessful faq creation or edit + * @param errorRes the errorRes handed to the alert service + */ + protected onSaveError(errorRes: HttpErrorResponse) { + this.isSaving = false; + if (errorRes.error && errorRes.error.title) { + this.alertService.addErrorAlert(errorRes.error.title, errorRes.error.message, errorRes.error.params); + } else { + onError(this.alertService, errorRes); + } + } + + updateCategories(categories: FaqCategory[]) { + this.faq.categories = categories; + this.exerciseCategories = categories; + } + + private loadCourseFaqCategories(courseId: number) { + loadCourseFaqCategories(courseId, this.alertService, this.faqService).subscribe((existingCategories) => { + this.existingCategories = existingCategories; + }); + } + + canSave(){ + return this.faq.questionTitle && this.faq.questionAnswer + } + +} diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html new file mode 100644 index 000000000000..b96feba1aa5d --- /dev/null +++ b/src/main/webapp/app/faq/faq.component.html @@ -0,0 +1,123 @@ +
+
+
+

+ +

+
+
+
+
+ +
    + @for (category of existingCategories; track category){ +
  • + +
  • + } +
+
+ +
+
+
+
+ @if (true) { +
+ + + + + + + + + + + + @for (faq of filteredFaq; track trackId(i, faq); let i = $index) { + + + + + + + + + } + +
+ + + + + + + + + + + +
+ {{ faq.id }} + + {{ faq.questionTitle }} + + {{ faq.questionAnswer }} + + @for (category of faq.categories; track category) { + + } + +
+
+ @if (true) { + + + + + } + @if (true) { + + } +
+
+
+
+ } +
diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts new file mode 100644 index 000000000000..27b185bc92c3 --- /dev/null +++ b/src/main/webapp/app/faq/faq.component.ts @@ -0,0 +1,144 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Faq } from 'app/entities/faq.model'; +import { + faEdit, + faFile, + faFileExport, + faFileImport, + faFilter, + faPencilAlt, + faPlus, + faPuzzlePiece, + faSort, + faTrash +} from '@fortawesome/free-solid-svg-icons'; +import { Subject } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { AlertService } from 'app/core/util/alert.service'; +import { ActivatedRoute } from '@angular/router'; +import { FaqService } from 'app/faq/faq.service'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { onError } from 'app/shared/util/global.utils'; +import { FaqCategory } from 'app/entities/faq-category.model'; +import { loadCourseFaqCategories } from 'app/faq/faq.utils'; +import { SortService } from 'app/shared/service/sort.service'; + +@Component({ + selector: 'jhi-faq', + templateUrl: './faq.component.html' + +}) + +export class FAQComponent implements OnInit, OnDestroy { + faqs: Faq[]; + filteredFaq: Faq[]; + existingCategories: FaqCategory[] + courseId: number; + + private dialogErrorSource = new Subject(); + dialogError$ = this.dialogErrorSource.asObservable(); + + activeFilters = new Set(); + predicate: string; + ascending: boolean; + + irisEnabled = false; + + // Icons + faEdit = faEdit; + faPlus = faPlus; + faFileImport = faFileImport; + faFileExport = faFileExport; + faTrash = faTrash; + faPencilAlt = faPencilAlt; + faFile = faFile; + faPuzzlePiece = faPuzzlePiece; + faFilter = faFilter; + faSort = faSort; + + constructor( + protected faqService: FaqService, + private route: ActivatedRoute, + private alertService: AlertService, + private sortService: SortService, + ) { + this.predicate = 'id'; + this.ascending = true; + } + + ngOnInit() { + this.courseId = Number(this.route.snapshot.paramMap.get('courseId')); + this.loadAll() + this.loadCourseExerciseCategories(this.courseId) + } + + ngOnDestroy(): void { + this.dialogErrorSource.unsubscribe(); + } + + trackId(index: number, item: Faq) { + return item.id; + } + + deleteFaq(faqId: number) { + this.faqService.delete(faqId).subscribe({ + next: () => + this.handleDeleteSuccess(faqId), + error: (error: HttpErrorResponse) => this.dialogErrorSource.next(error.message), + }); + } + + private handleDeleteSuccess(faqId: number) { + this.faqs = this.faqs.filter(faq => faq.id !== faqId); + this.dialogErrorSource.next(''); + this.applyFilters(); + } + + toggleFilters(category: String) { + this.activeFilters.has(category)? this.activeFilters.delete(category) : this.activeFilters.add(category) + this.applyFilters(); + } + + sortRows() { + this.sortService.sortByProperty(this.filteredFaq, this.predicate, this.ascending); + } + + private loadAll() { + this.faqService.findAllByCourseId(this.courseId) + .pipe( + map((res: HttpResponse) => res.body), + ) + .subscribe({ + next: (res: Faq[]) => { + this.faqs = res; + this.applyFilters() + }, + error: (res: HttpErrorResponse) => onError(this.alertService, res), + }); + } + + private loadCourseExerciseCategories(courseId: number) { + loadCourseFaqCategories(courseId, this.alertService, this.faqService).subscribe((existingCategories) => { + this.existingCategories = existingCategories; + }); + } + + + private applyFilters(): void { + if (this.activeFilters.size === 0) { + // If no filters selected, show all faqs + this.filteredFaq = this.faqs; + } else { + this.filteredFaq = this.faqs.filter((faq) => this.hasFilteredCategory(faq, this.activeFilters)); + } + + } + + public hasFilteredCategory(faq: Faq, filteredCategory: Set){ + let categories = faq.categories?.map((category) => category.category) + if(categories){ + return categories.some(category => filteredCategory.has(category!)); + } + + } +} diff --git a/src/main/webapp/app/faq/faq.module.ts b/src/main/webapp/app/faq/faq.module.ts new file mode 100644 index 000000000000..09e80ef26682 --- /dev/null +++ b/src/main/webapp/app/faq/faq.module.ts @@ -0,0 +1,34 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { CompetencyFormComponent } from 'app/course/competencies/forms/competency/competency-form.component'; +import { FAQComponent } from 'app/faq/faq.component'; +import { faqRoutes } from 'app/faq/faq.routes'; +import { FAQUpdateComponent } from 'app/faq/faq-update.component'; +import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; +import { FormDateTimePickerModule } from 'app/shared/date-time-picker/date-time-picker.module'; +import { ArtemisCategorySelectorModule } from 'app/shared/category-selector/category-selector.module'; +import { + CustomExerciseCategoryBadgeComponent +} from "app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component"; +const ENTITY_STATES = [...faqRoutes]; + +@NgModule({ + imports: [ + ArtemisSharedModule, + RouterModule.forChild(ENTITY_STATES), + ArtemisSharedComponentModule, + CompetencyFormComponent, + ArtemisMarkdownEditorModule, + FormDateTimePickerModule, + ArtemisCategorySelectorModule, + CustomExerciseCategoryBadgeComponent, + + ], + declarations: [ + FAQUpdateComponent, + FAQComponent + ], +}) +export class ArtemisFAQModule {} diff --git a/src/main/webapp/app/faq/faq.routes.ts b/src/main/webapp/app/faq/faq.routes.ts new file mode 100644 index 000000000000..ed772543c6d6 --- /dev/null +++ b/src/main/webapp/app/faq/faq.routes.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Resolve, Routes } from '@angular/router'; +import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; +import { Observable, of } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { Authority } from 'app/shared/constants/authority.constants'; +import { CourseManagementResolve } from 'app/course/manage/course-management-resolve.service'; +import { CourseManagementTabBarComponent } from 'app/course/manage/course-management-tab-bar/course-management-tab-bar.component'; +import { FAQComponent } from 'app/faq/faq.component'; +import { FaqService } from 'app/faq/faq.service'; +import { Faq } from 'app/entities/faq.model'; +import { FAQUpdateComponent } from 'app/faq/faq-update.component'; + + +@Injectable({ providedIn: 'root' }) +export class FAQResolve implements Resolve { + constructor(private faqService: FaqService) {} + + resolve(route: ActivatedRouteSnapshot): Observable { + const faqId = route.params['faqId']; + if (faqId) { + return this.faqService.find(faqId).pipe( + filter((response: HttpResponse) => response.ok), + map((faq: HttpResponse) => faq.body!), + ); + } + return of(new Faq()); + } +} + + +export const faqRoutes: Routes = [ + { + path: ':courseId/faqs', + component: CourseManagementTabBarComponent, + children: [ + { + path: '', + component: FAQComponent, + resolve: { + course: CourseManagementResolve, + }, + data: { + authorities: [Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], + pageTitle: '', + }, + canActivate: [UserRouteAccessService], + }, + { + // Create a new path without a component defined to prevent the FAQ from being always rendered + path: '', + resolve: { + course: CourseManagementResolve, + }, + children: [ + { + path: 'new', + component: FAQUpdateComponent, + data: { + authorities: [Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], + pageTitle: 'global.generic.create', + + }, + canActivate: [UserRouteAccessService], + }, + { + path: ':faqId', + resolve: { + faq: FAQResolve, + }, + children: [ + { + path: 'edit', + component: FAQUpdateComponent, + data: { + authorities: [Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], + pageTitle: 'global.generic.edit', + }, + canActivate: [UserRouteAccessService], + }, + + ], + }, + ], + }, + ], + }, +]; diff --git a/src/main/webapp/app/faq/faq.service.ts b/src/main/webapp/app/faq/faq.service.ts new file mode 100644 index 000000000000..70b6bba4fc01 --- /dev/null +++ b/src/main/webapp/app/faq/faq.service.ts @@ -0,0 +1,149 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Faq, FaqState } from 'app/entities/faq.model' +import { Exercise } from 'app/entities/exercise.model'; +import { FaqCategory } from 'app/entities/faq-category.model'; +import { AlertService } from 'app/core/util/alert.service'; +import { ExerciseCategory } from 'app/entities/exercise-category.model'; + +type EntityResponseType = HttpResponse; +type EntityArrayResponseType = HttpResponse; + + +@Injectable({ providedIn: 'root' }) +export class FaqService { + + public resourceUrl = 'api/courses'; + + constructor( + protected http: HttpClient, + protected alertService: AlertService + + ) {} + + create(faq: Faq): Observable{ + let copy = FaqService.convertFaqFromClient(faq) + faq.faqState = FaqState.ACCEPTED + return this.http.post( `api/faqs`,copy, { observe: 'response' }).pipe( + map((res: EntityResponseType) => { + return res; + }), + ); + + } + + update(faq: Faq): Observable{ + let copy = FaqService.convertFaqFromClient(faq) + return this.http.put(`api/faqs/${faq.id}`, copy, { observe: 'response' }).pipe( + map((res: EntityResponseType) => { + return res; + }), + ); + + } + + find(faqId: number): Observable { + return this.http.get(`api/faqs/${faqId}`, { observe: 'response' }).pipe( + map((res: EntityResponseType) => + FaqService.convertFaqCategoriesFromServer(res) + ), + ); + } + + + findAllByCourseId(courseId: number): Observable { + return this.http + .get(this.resourceUrl+`/${courseId}/faqs`, { + observe: 'response', + }) + .pipe( + map((res: EntityArrayResponseType) => FaqService.convertExerciseCategoryArrayFromServer(res)) + ); + } + + delete(faqId: number): Observable>{ + return this.http.delete(`api/faqs/${faqId}`, { observe: 'response' }) + } + + findAllCategoriesByCourseId(courseId: number) { + return this.http.get(this.resourceUrl+`/${courseId}/faq-categories`, { + observe: 'response', + }) + } + /** + * Converts the faq category json string into FaqCategory objects (if it exists). + * @param res the response + */ + static convertFaqCategoriesFromServer(res: ERT): ERT { + if (res.body && res.body.categories) { + FaqService.parseExerciseCategories(res.body); + } + return res; + } + + /** + * Converts a faqs categories into a json string (to send them to the server). Does nothing if no categories exist + * @param faq the faq + */ + static stringifyFaqCategories(faq: Faq) { + return faq.categories?.map((category) => JSON.stringify(category) as unknown as FaqCategory); + } + + convertFaqCategoriesAsStringFromServer(categories: string[]): ExerciseCategory[] { + return categories.map((category) => JSON.parse(category)); + } + + /** + * Converts the faq category json strings into FaqCategory objects (if it exists). + * @param res the response + */ + static convertExerciseCategoryArrayFromServer(res: EART): EART { + if (res.body) { + res.body.forEach((exercise: E) => FaqService.parseExerciseCategories(exercise)); + } + return res; + } + + /** + * Parses the faq categories JSON string into {@link FaqCategory} objects. + * @param faq - the exercise + */ + static parseExerciseCategories(faq?: Faq) { + if (faq?.categories) { + faq.categories = faq.categories.map((category) => { + const categoryObj = JSON.parse(category as unknown as string); + return new FaqCategory(categoryObj.category, categoryObj.color); + }); + } + } + + static parseFaqCategoriesString(categories?: String[]) { + let faqCategories: FaqCategory[] = [] + if (categories) { + faqCategories = categories.map((category) => { + const categoryObj = JSON.parse(category as unknown as string); + return new FaqCategory(categoryObj.category, categoryObj.color); + }); + + } + return faqCategories + } + + /** + * Prepare client-faq to be uploaded to the server + * @param { Faq } faq - faq that will be modified + */ + static convertFaqFromClient(faq: F): Faq { + let copy = Object.assign(faq, {}); + copy.categories = FaqService.stringifyFaqCategories(copy); + if (copy.categories) { + + } + return copy; + } + + + +} diff --git a/src/main/webapp/app/faq/faq.utils.ts b/src/main/webapp/app/faq/faq.utils.ts new file mode 100644 index 000000000000..deb21fff6c91 --- /dev/null +++ b/src/main/webapp/app/faq/faq.utils.ts @@ -0,0 +1,33 @@ +import { onError } from 'app/shared/util/global.utils'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { AlertService } from 'app/core/util/alert.service'; +import { Observable } from 'rxjs'; +import { ExerciseCategory } from 'app/entities/exercise-category.model'; +import { FaqService } from 'app/faq/faq.service'; +import { FaqCategory } from 'app/entities/faq-category.model'; + +export function loadCourseFaqCategories( + courseId: number | undefined, + alertService: AlertService, + faqService: FaqService +): Observable { + if (courseId === undefined) { + return new Observable((observer) => { + observer.complete(); + }); + } + + return new Observable((observer) => { + faqService.findAllCategoriesByCourseId(courseId).subscribe({ + next: (categoryRes: HttpResponse) => { + const existingCategories = faqService.convertFaqCategoriesAsStringFromServer(categoryRes.body!); + observer.next(existingCategories); + observer.complete(); + }, + error: (error: HttpErrorResponse) => { + onError(alertService, error); + observer.complete(); + }, + }); + }); +} diff --git a/src/main/webapp/app/shared/category-selector/category-selector.component.ts b/src/main/webapp/app/shared/category-selector/category-selector.component.ts index d899a7b034b3..4214f340ffca 100644 --- a/src/main/webapp/app/shared/category-selector/category-selector.component.ts +++ b/src/main/webapp/app/shared/category-selector/category-selector.component.ts @@ -7,6 +7,7 @@ import { FormControl } from '@angular/forms'; import { MatChipInputEvent } from '@angular/material/chips'; import { Observable, map, startWith } from 'rxjs'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { FaqCategory } from 'app/entities/faq-category.model'; const DEFAULT_COLORS = ['#6ae8ac', '#9dca53', '#94a11c', '#691b0b', '#ad5658', '#1b97ca', '#0d3cc2', '#0ab84f']; @@ -22,12 +23,12 @@ export class CategorySelectorComponent implements OnChanges { /** * the selected categories, which can be manipulated by the user in the UI */ - @Input() categories: ExerciseCategory[]; + @Input() categories: ExerciseCategory[] | FaqCategory[]; /** * the existing categories used for auto-completion, might include duplicates */ - @Input() existingCategories: ExerciseCategory[]; + @Input() existingCategories: ExerciseCategory[] | FaqCategory[]; @Output() selectedCategories = new EventEmitter(); diff --git a/src/main/webapp/app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component.ts b/src/main/webapp/app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component.ts index 8ba41f96ba8b..aec203a26946 100644 --- a/src/main/webapp/app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component.ts +++ b/src/main/webapp/app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component.ts @@ -3,6 +3,7 @@ import type { ExerciseCategory } from 'app/entities/exercise-category.model'; import { CommonModule } from '@angular/common'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { FaqCategory } from 'app/entities/faq-category.model'; type CategoryFontSize = 'default' | 'small'; @@ -16,7 +17,7 @@ type CategoryFontSize = 'default' | 'small'; export class CustomExerciseCategoryBadgeComponent { protected readonly faTimes = faTimes; - @Input({ required: true }) category: ExerciseCategory; + @Input({ required: true }) category: ExerciseCategory | FaqCategory; @Input() displayRemoveButton: boolean = false; @Input() onClick: () => void = () => {}; @Input() fontSize: CategoryFontSize = 'default'; From f96f6107e6a1835dbd0bc9f3f082cf79fa55ad65 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 10 Sep 2024 20:00:44 +0200 Subject: [PATCH 09/68] refactored toggleFilters to make commits work --- src/main/webapp/app/faq/faq.component.ts | 70 ++++++++++-------------- 1 file changed, 28 insertions(+), 42 deletions(-) diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index 27b185bc92c3..37cf46143595 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -1,17 +1,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Faq } from 'app/entities/faq.model'; -import { - faEdit, - faFile, - faFileExport, - faFileImport, - faFilter, - faPencilAlt, - faPlus, - faPuzzlePiece, - faSort, - faTrash -} from '@fortawesome/free-solid-svg-icons'; +import { faEdit, faFile, faFileExport, faFileImport, faFilter, faPencilAlt, faPlus, faPuzzlePiece, faSort, faTrash } from '@fortawesome/free-solid-svg-icons'; import { Subject } from 'rxjs'; import { map } from 'rxjs/operators'; import { AlertService } from 'app/core/util/alert.service'; @@ -25,20 +14,18 @@ import { SortService } from 'app/shared/service/sort.service'; @Component({ selector: 'jhi-faq', - templateUrl: './faq.component.html' - + templateUrl: './faq.component.html', }) - export class FAQComponent implements OnInit, OnDestroy { faqs: Faq[]; filteredFaq: Faq[]; - existingCategories: FaqCategory[] + existingCategories: FaqCategory[]; courseId: number; private dialogErrorSource = new Subject(); dialogError$ = this.dialogErrorSource.asObservable(); - activeFilters = new Set(); + activeFilters = new Set(); predicate: string; ascending: boolean; @@ -68,8 +55,8 @@ export class FAQComponent implements OnInit, OnDestroy { ngOnInit() { this.courseId = Number(this.route.snapshot.paramMap.get('courseId')); - this.loadAll() - this.loadCourseExerciseCategories(this.courseId) + this.loadAll(); + this.loadCourseExerciseCategories(this.courseId); } ngOnDestroy(): void { @@ -82,20 +69,23 @@ export class FAQComponent implements OnInit, OnDestroy { deleteFaq(faqId: number) { this.faqService.delete(faqId).subscribe({ - next: () => - this.handleDeleteSuccess(faqId), + next: () => this.handleDeleteSuccess(faqId), error: (error: HttpErrorResponse) => this.dialogErrorSource.next(error.message), }); } private handleDeleteSuccess(faqId: number) { - this.faqs = this.faqs.filter(faq => faq.id !== faqId); + this.faqs = this.faqs.filter((faq) => faq.id !== faqId); this.dialogErrorSource.next(''); this.applyFilters(); } - toggleFilters(category: String) { - this.activeFilters.has(category)? this.activeFilters.delete(category) : this.activeFilters.add(category) + toggleFilters(category: string) { + if (this.activeFilters.has(category)) { + this.activeFilters.delete(category); + } else { + this.activeFilters.add(category); + } this.applyFilters(); } @@ -104,17 +94,16 @@ export class FAQComponent implements OnInit, OnDestroy { } private loadAll() { - this.faqService.findAllByCourseId(this.courseId) - .pipe( - map((res: HttpResponse) => res.body), - ) - .subscribe({ - next: (res: Faq[]) => { - this.faqs = res; - this.applyFilters() - }, - error: (res: HttpErrorResponse) => onError(this.alertService, res), - }); + this.faqService + .findAllByCourseId(this.courseId) + .pipe(map((res: HttpResponse) => res.body)) + .subscribe({ + next: (res: Faq[]) => { + this.faqs = res; + this.applyFilters(); + }, + error: (res: HttpErrorResponse) => onError(this.alertService, res), + }); } private loadCourseExerciseCategories(courseId: number) { @@ -123,7 +112,6 @@ export class FAQComponent implements OnInit, OnDestroy { }); } - private applyFilters(): void { if (this.activeFilters.size === 0) { // If no filters selected, show all faqs @@ -131,14 +119,12 @@ export class FAQComponent implements OnInit, OnDestroy { } else { this.filteredFaq = this.faqs.filter((faq) => this.hasFilteredCategory(faq, this.activeFilters)); } - } - public hasFilteredCategory(faq: Faq, filteredCategory: Set){ - let categories = faq.categories?.map((category) => category.category) - if(categories){ - return categories.some(category => filteredCategory.has(category!)); + public hasFilteredCategory(faq: Faq, filteredCategory: Set) { + const categories = faq.categories?.map((category) => category.category); + if (categories) { + return categories.some((category) => filteredCategory.has(category!)); } - } } From af392dbe7d463f7d7bcea46948ddecda69f0f191 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 11 Sep 2024 16:17:33 +0200 Subject: [PATCH 10/68] Added integration test, but they do not work yet --- .../tum/in/www1/artemis/faq/FaqFactory.java | 29 ++++ .../www1/artemis/faq/FaqIntegrationTest.java | 124 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java create mode 100644 src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java diff --git a/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java b/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java new file mode 100644 index 000000000000..64e08ae1742b --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java @@ -0,0 +1,29 @@ +package de.tum.in.www1.artemis.faq; + +import java.util.HashSet; +import java.util.Set; + +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.Faq; +import de.tum.in.www1.artemis.domain.FaqState; + +public class FaqFactory { + + public static Faq generateFaq(Long id, Course course) { + Faq faq = new Faq(); + faq.setId(id); + faq.setCourse(course); + faq.setFaqState(FaqState.ACCEPTED); + faq.setQuestionAnswer("Answer"); + faq.setQuestionTitle("Title"); + faq.setCategories(generateFaqCategories()); + return faq; + } + + public static Set generateFaqCategories() { + HashSet categories = new HashSet<>(); + categories.add("this is a category"); + categories.add("this is also a category"); + return categories; + } +} diff --git a/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java new file mode 100644 index 000000000000..89c9c8f5b11b --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java @@ -0,0 +1,124 @@ +package de.tum.in.www1.artemis.faq; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; + +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.Faq; +import de.tum.in.www1.artemis.domain.FaqState; +import de.tum.in.www1.artemis.repository.FaqRepository; + +class FaqIntegrationTest extends AbstractSpringIntegrationIndependentTest { + + private static final String TEST_PREFIX = "faqIntegrationTest"; + + @Autowired + private FaqRepository faqRepository; + + private Course course1; + + private Faq faq; + + @BeforeEach + void initTestCase() { + int numberOfTutors = 2; + long courseId = 2; + long faqId = 1; + userUtilService.addUsers(TEST_PREFIX, 1, numberOfTutors, 0, 1); + this.course1 = courseUtilService.createCourse(courseId); + this.faq = FaqFactory.generateFaq(faqId, course1); + faqRepository.save(this.faq); + this.course1.addFaq(this.faq); + // Add users that are not in the course + userUtilService.createAndSaveUser(TEST_PREFIX + "student42"); + userUtilService.createAndSaveUser(TEST_PREFIX + "instructor42"); + } + + private void testAllPreAuthorize() throws Exception { + request.postWithResponseBody("/api/faqs", new Faq(), Faq.class, HttpStatus.FORBIDDEN); + System.out.println("Test"); + request.putWithResponseBody("/api/faqs/" + faq.getId(), new Faq(), Faq.class, HttpStatus.FORBIDDEN); + request.getList("/api/courses/" + course1.getId() + "/faqs", HttpStatus.FORBIDDEN, Faq.class); + request.delete("/api/faqs/" + faq.getId(), HttpStatus.FORBIDDEN); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") + void testAll_asTutor() throws Exception { + this.testAllPreAuthorize(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testAll_asStudent() throws Exception { + this.testAllPreAuthorize(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void createFaq_correctRequestBody_shouldCreateFaq() throws Exception { + Course course = courseRepository.findByIdElseThrow(this.course1.getId()); + + Faq faq = new Faq(); + faq.setQuestionTitle("Title"); + faq.setQuestionAnswer("Answer"); + faq.setCategories(FaqFactory.generateFaqCategories()); + faq.setFaqState(FaqState.ACCEPTED); + faq.setCourse(course); + + Faq returnedFaq = request.postWithResponseBody("/api/faqs", faq, Faq.class, HttpStatus.CREATED); + + assertThat(returnedFaq).isNotNull(); + assertThat(returnedFaq.getId()).isNotNull(); + assertThat(returnedFaq.getQuestionTitle()).isEqualTo(faq.getQuestionTitle()); + assertThat(returnedFaq.getCourse().getId()).isEqualTo(faq.getCourse().getId()); + assertThat(returnedFaq.getQuestionAnswer()).isEqualTo(faq.getQuestionAnswer()); + assertThat(returnedFaq.getCategories()).isEqualTo(faq.getCategories()); + assertThat(returnedFaq.getFaqState()).isEqualTo(faq.getFaqState()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void createFaq_alreadyId_shouldReturnBadRequest() throws Exception { + Faq faq = new Faq(); + faq.setId(1L); + request.postWithResponseBody("/api/faqs", faq, Faq.class, HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void updateFaq_correctRequestBody_shouldUpdateFaq() throws Exception { + Faq faq = faqRepository.findById(this.faq.getId()).orElseThrow(); + faq.setQuestionTitle("Updated"); + faq.setQuestionAnswer("Updated"); + faq.setFaqState(FaqState.PROPOSED); + Set newCategories = new HashSet(); + newCategories.add("Test"); + faq.setCategories(newCategories); + Faq updatedFaq = request.putWithResponseBody("/api/faqs/" + faq.getId(), faq, Faq.class, HttpStatus.OK); + + assertThat(updatedFaq.getQuestionTitle()).isEqualTo("Updated"); + assertThat(updatedFaq.getQuestionTitle()).isEqualTo("Updated"); + assertThat(updatedFaq.getFaqState()).isEqualTo(FaqState.REJECTED); + assertThat(updatedFaq.getCategories()).isEqualTo(newCategories); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetFaqCategoriesByCourseId() throws Exception { + Faq faq = faqRepository.findById(this.faq.getId()).orElseThrow(); + Set categories = faq.getCategories(); + Set returnedCategories = request.get("/api/courses/" + faq.getCourse().getId() + "/faq-categories", HttpStatus.OK, Set.class); + assertThat(categories).isEqualTo(returnedCategories); + } + +} From 1c29060c77863b3388f5bcd0292265fbe7101710 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 11 Sep 2024 16:32:11 +0200 Subject: [PATCH 11/68] Integration Tests --- src/main/java/de/tum/in/www1/artemis/service/FaqService.java | 2 +- .../java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/FaqService.java b/src/main/java/de/tum/in/www1/artemis/service/FaqService.java index 0f213a1fc8e2..0a47ed169292 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/FaqService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/FaqService.java @@ -18,7 +18,7 @@ public FaqService(FaqRepository faqRepository) { } /** - * Deletes the given lecture (with its lecture units). + * Deletes the given faq * * @param faqId the faqId of to be deleted faq */ diff --git a/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java index 89c9c8f5b11b..720a658daced 100644 --- a/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java @@ -38,9 +38,6 @@ void initTestCase() { this.faq = FaqFactory.generateFaq(faqId, course1); faqRepository.save(this.faq); this.course1.addFaq(this.faq); - // Add users that are not in the course - userUtilService.createAndSaveUser(TEST_PREFIX + "student42"); - userUtilService.createAndSaveUser(TEST_PREFIX + "instructor42"); } private void testAllPreAuthorize() throws Exception { From 6e80b3bc88e61316cd0730d4fdd546eb0b0a76a6 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 12 Sep 2024 12:43:57 +0200 Subject: [PATCH 12/68] Integration Tests fixed. Why so ever its works now --- .../tum/in/www1/artemis/faq/FaqFactory.java | 3 +- .../www1/artemis/faq/FaqIntegrationTest.java | 49 ++++++++----------- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java b/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java index 64e08ae1742b..815940598395 100644 --- a/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java +++ b/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java @@ -9,9 +9,8 @@ public class FaqFactory { - public static Faq generateFaq(Long id, Course course) { + public static Faq generateFaq(Course course) { Faq faq = new Faq(); - faq.setId(id); faq.setCourse(course); faq.setFaqState(FaqState.ACCEPTED); faq.setQuestionAnswer("Answer"); diff --git a/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java index 720a658daced..b3f62be75e70 100644 --- a/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.util.HashSet; +import java.util.List; import java.util.Set; import org.junit.jupiter.api.BeforeEach; @@ -19,7 +20,7 @@ class FaqIntegrationTest extends AbstractSpringIntegrationIndependentTest { - private static final String TEST_PREFIX = "faqIntegrationTest"; + private static final String TEST_PREFIX = "faqintegrationtest"; @Autowired private FaqRepository faqRepository; @@ -29,23 +30,24 @@ class FaqIntegrationTest extends AbstractSpringIntegrationIndependentTest { private Faq faq; @BeforeEach - void initTestCase() { + void initTestCase() throws Exception { int numberOfTutors = 2; - long courseId = 2; - long faqId = 1; userUtilService.addUsers(TEST_PREFIX, 1, numberOfTutors, 0, 1); - this.course1 = courseUtilService.createCourse(courseId); - this.faq = FaqFactory.generateFaq(faqId, course1); + List courses = courseUtilService.createCoursesWithExercisesAndLectures(TEST_PREFIX, true, true, numberOfTutors); + this.course1 = this.courseRepository.findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(courses.getFirst().getId()); + this.faq = FaqFactory.generateFaq(course1); faqRepository.save(this.faq); - this.course1.addFaq(this.faq); + // Add users that are not in the course + userUtilService.createAndSaveUser(TEST_PREFIX + "student42"); + userUtilService.createAndSaveUser(TEST_PREFIX + "instructor42"); + } private void testAllPreAuthorize() throws Exception { request.postWithResponseBody("/api/faqs", new Faq(), Faq.class, HttpStatus.FORBIDDEN); - System.out.println("Test"); - request.putWithResponseBody("/api/faqs/" + faq.getId(), new Faq(), Faq.class, HttpStatus.FORBIDDEN); + request.putWithResponseBody("/api/faqs/" + this.faq.getId(), this.faq, Faq.class, HttpStatus.FORBIDDEN); request.getList("/api/courses/" + course1.getId() + "/faqs", HttpStatus.FORBIDDEN, Faq.class); - request.delete("/api/faqs/" + faq.getId(), HttpStatus.FORBIDDEN); + request.delete("/api/faqs/" + this.faq.getId(), HttpStatus.FORBIDDEN); } @Test @@ -63,24 +65,15 @@ void testAll_asStudent() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createFaq_correctRequestBody_shouldCreateFaq() throws Exception { - Course course = courseRepository.findByIdElseThrow(this.course1.getId()); - - Faq faq = new Faq(); - faq.setQuestionTitle("Title"); - faq.setQuestionAnswer("Answer"); - faq.setCategories(FaqFactory.generateFaqCategories()); - faq.setFaqState(FaqState.ACCEPTED); - faq.setCourse(course); - - Faq returnedFaq = request.postWithResponseBody("/api/faqs", faq, Faq.class, HttpStatus.CREATED); - + Faq newFaq = FaqFactory.generateFaq(course1); + Faq returnedFaq = request.postWithResponseBody("/api/faqs", newFaq, Faq.class, HttpStatus.CREATED); assertThat(returnedFaq).isNotNull(); assertThat(returnedFaq.getId()).isNotNull(); - assertThat(returnedFaq.getQuestionTitle()).isEqualTo(faq.getQuestionTitle()); - assertThat(returnedFaq.getCourse().getId()).isEqualTo(faq.getCourse().getId()); - assertThat(returnedFaq.getQuestionAnswer()).isEqualTo(faq.getQuestionAnswer()); - assertThat(returnedFaq.getCategories()).isEqualTo(faq.getCategories()); - assertThat(returnedFaq.getFaqState()).isEqualTo(faq.getFaqState()); + assertThat(returnedFaq.getQuestionTitle()).isEqualTo(newFaq.getQuestionTitle()); + assertThat(returnedFaq.getCourse().getId()).isEqualTo(newFaq.getCourse().getId()); + assertThat(returnedFaq.getQuestionAnswer()).isEqualTo(newFaq.getQuestionAnswer()); + assertThat(returnedFaq.getCategories()).isEqualTo(newFaq.getCategories()); + assertThat(returnedFaq.getFaqState()).isEqualTo(newFaq.getFaqState()); } @Test @@ -98,14 +91,14 @@ void updateFaq_correctRequestBody_shouldUpdateFaq() throws Exception { faq.setQuestionTitle("Updated"); faq.setQuestionAnswer("Updated"); faq.setFaqState(FaqState.PROPOSED); - Set newCategories = new HashSet(); + Set newCategories = new HashSet<>(); newCategories.add("Test"); faq.setCategories(newCategories); Faq updatedFaq = request.putWithResponseBody("/api/faqs/" + faq.getId(), faq, Faq.class, HttpStatus.OK); assertThat(updatedFaq.getQuestionTitle()).isEqualTo("Updated"); assertThat(updatedFaq.getQuestionTitle()).isEqualTo("Updated"); - assertThat(updatedFaq.getFaqState()).isEqualTo(FaqState.REJECTED); + assertThat(updatedFaq.getFaqState()).isEqualTo(FaqState.PROPOSED); assertThat(updatedFaq.getCategories()).isEqualTo(newCategories); } From 4a9609c0c60d1f54dad3abd557f4daf42a2ffb2c Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 12 Sep 2024 12:48:18 +0200 Subject: [PATCH 13/68] Formula Action change from Patrick --- .../manage/course-update.component.html | 29 ++++++------------ .../course/manage/course-update.component.ts | 11 +++---- .../webapp/app/faq/faq-update.component.ts | 30 ++++++++----------- 3 files changed, 25 insertions(+), 45 deletions(-) diff --git a/src/main/webapp/app/course/manage/course-update.component.html b/src/main/webapp/app/course/manage/course-update.component.html index 4906832adf6c..5af60571f1fd 100644 --- a/src/main/webapp/app/course/manage/course-update.component.html +++ b/src/main/webapp/app/course/manage/course-update.component.html @@ -347,26 +347,6 @@
ngbTooltip="{{ 'artemisApp.course.courseCommunicationSetting.messagingEnabled.tooltip' | artemisTranslate }}" /> -
- - - -
@if (communicationEnabled) {
@@ -393,6 +373,15 @@
+
+ + + +
@if (this.isAdmin) {
diff --git a/src/main/webapp/app/course/manage/course-update.component.ts b/src/main/webapp/app/course/manage/course-update.component.ts index 69549695b862..8d928236c268 100644 --- a/src/main/webapp/app/course/manage/course-update.component.ts +++ b/src/main/webapp/app/course/manage/course-update.component.ts @@ -71,7 +71,7 @@ export class CourseUpdateComponent implements OnInit { faExclamationTriangle = faExclamationTriangle; faPen = faPen; - faqEnabled = true + faqEnabled = true; communicationEnabled = true; messagingEnabled = true; ltiEnabled = false; @@ -116,7 +116,7 @@ export class CourseUpdateComponent implements OnInit { this.courseOrganizations = organizations; }); this.originalTimeZone = this.course.timeZone; - this.faqEnabled = course.faqEnabled + this.faqEnabled = course.faqEnabled; // complaints are only enabled when at least one complaint is allowed and the complaint duration is positive this.complaintsEnabled = (this.course.maxComplaints! > 0 || this.course.maxTeamComplaints! > 0) && @@ -296,13 +296,12 @@ export class CourseUpdateComponent implements OnInit { if (this.communicationEnabled && this.messagingEnabled) { course['courseInformationSharingConfiguration'] = CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING; - course.faqEnabled = this.faqEnabled + course.faqEnabled = this.faqEnabled; } else if (this.communicationEnabled && !this.messagingEnabled) { course['courseInformationSharingConfiguration'] = CourseInformationSharingConfiguration.COMMUNICATION_ONLY; - course.faqEnabled = this.faqEnabled + course.faqEnabled = this.faqEnabled; } else { this.communicationEnabled = false; - this.faqEnabled = false course['courseInformationSharingConfiguration'] = CourseInformationSharingConfiguration.DISABLED; } @@ -654,9 +653,7 @@ export class CourseUpdateComponent implements OnInit { disableMessaging() { this.messagingEnabled = false; - this.faqEnabled = false } - } const CourseValidator: ValidatorFn = (formGroup: FormGroup) => { diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index a9b796614a0e..97c782220858 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -8,7 +8,7 @@ import { Course } from 'app/entities/course.model'; import { onError } from 'app/shared/util/global.utils'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { faBan, faQuestionCircle, faSave } from '@fortawesome/free-solid-svg-icons'; -import { MonacoFormulaAction } from 'app/shared/monaco-editor/model/actions/monaco-formula.action'; +import { FormulaAction } from 'app/shared/monaco-editor/model/actions/formula.action'; import { Faq } from 'app/entities/faq.model'; import { FaqService } from 'app/faq/faq.service'; @@ -21,15 +21,14 @@ import { loadCourseFaqCategories } from 'app/faq/faq.utils'; styleUrls: ['./faq-update.component.scss'], }) export class FAQUpdateComponent implements OnInit { - faq: Faq; isSaving: boolean; - existingCategories: FaqCategory[] = [] - exerciseCategories: FaqCategory[] = [] + existingCategories: FaqCategory[] = []; + exerciseCategories: FaqCategory[] = []; courses: Course[]; - domainActionsDescription = [new MonacoFormulaAction()]; + domainActionsDescription = [new FormulaAction()]; file: File; fileName: string; @@ -40,7 +39,7 @@ export class FAQUpdateComponent implements OnInit { constructor( protected alertService: AlertService, - protected faqService : FaqService, + protected faqService: FaqService, protected courseService: CourseManagementService, protected activatedRoute: ActivatedRoute, private navigationUtilService: ArtemisNavigationUtilService, @@ -59,13 +58,12 @@ export class FAQUpdateComponent implements OnInit { const course = data['course']; if (course) { this.faq.course = course; - this.loadCourseFaqCategories(course.id) + this.loadCourseFaqCategories(course.id); } - if(faq.categories){ - this.exerciseCategories = faq.categories + if (faq.categories) { + this.exerciseCategories = faq.categories; } }); - } /** @@ -87,9 +85,8 @@ export class FAQUpdateComponent implements OnInit { this.subscribeToSaveResponse(this.faqService.update(this.faq)); } else { // Newly created faq must have a channel name, which cannot be undefined - console.log(this.faq) + console.log(this.faq); this.subscribeToSaveResponse(this.faqService.create(this.faq)); - } } @@ -113,11 +110,9 @@ export class FAQUpdateComponent implements OnInit { this.isSaving = false; this.faq = response.body!; this.alertService.success(`FAQ with title ${faq.questionTitle} was successfully created.`); - }, }); - } - else { + } else { this.isSaving = false; this.router.navigate(['course-management', faq.course!.id, 'faqs']); } @@ -147,8 +142,7 @@ export class FAQUpdateComponent implements OnInit { }); } - canSave(){ - return this.faq.questionTitle && this.faq.questionAnswer + canSave() { + return this.faq.questionTitle && this.faq.questionAnswer; } - } From f276f9b46c8e1e56274d8b5125db624c3a5ec1b8 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 12 Sep 2024 14:02:06 +0200 Subject: [PATCH 14/68] Make components standalone --- src/main/webapp/app/faq/faq-update.component.ts | 6 ++++++ src/main/webapp/app/faq/faq.component.ts | 5 +++++ src/main/webapp/app/faq/faq.module.ts | 14 +++----------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index 97c782220858..5cd81aa80fd7 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -14,11 +14,17 @@ import { FaqService } from 'app/faq/faq.service'; import { FaqCategory } from 'app/entities/faq-category.model'; import { loadCourseFaqCategories } from 'app/faq/faq.utils'; +import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; +import { ArtemisCategorySelectorModule } from 'app/shared/category-selector/category-selector.module'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; @Component({ selector: 'jhi-faq-update', templateUrl: './faq-update.component.html', styleUrls: ['./faq-update.component.scss'], + standalone: true, + imports: [ArtemisSharedModule, ArtemisSharedComponentModule, ArtemisMarkdownEditorModule, ArtemisCategorySelectorModule], }) export class FAQUpdateComponent implements OnInit { faq: Faq; diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index 37cf46143595..b942c250dc90 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -11,10 +11,15 @@ import { onError } from 'app/shared/util/global.utils'; import { FaqCategory } from 'app/entities/faq-category.model'; import { loadCourseFaqCategories } from 'app/faq/faq.utils'; import { SortService } from 'app/shared/service/sort.service'; +import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; @Component({ selector: 'jhi-faq', templateUrl: './faq.component.html', + standalone: true, + imports: [ArtemisSharedModule, CustomExerciseCategoryBadgeComponent, ArtemisSharedComponentModule], }) export class FAQComponent implements OnInit, OnDestroy { faqs: Faq[]; diff --git a/src/main/webapp/app/faq/faq.module.ts b/src/main/webapp/app/faq/faq.module.ts index 09e80ef26682..bb13f966e3b5 100644 --- a/src/main/webapp/app/faq/faq.module.ts +++ b/src/main/webapp/app/faq/faq.module.ts @@ -2,16 +2,12 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; -import { CompetencyFormComponent } from 'app/course/competencies/forms/competency/competency-form.component'; import { FAQComponent } from 'app/faq/faq.component'; import { faqRoutes } from 'app/faq/faq.routes'; import { FAQUpdateComponent } from 'app/faq/faq-update.component'; import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; -import { FormDateTimePickerModule } from 'app/shared/date-time-picker/date-time-picker.module'; import { ArtemisCategorySelectorModule } from 'app/shared/category-selector/category-selector.module'; -import { - CustomExerciseCategoryBadgeComponent -} from "app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component"; +import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; const ENTITY_STATES = [...faqRoutes]; @NgModule({ @@ -19,16 +15,12 @@ const ENTITY_STATES = [...faqRoutes]; ArtemisSharedModule, RouterModule.forChild(ENTITY_STATES), ArtemisSharedComponentModule, - CompetencyFormComponent, ArtemisMarkdownEditorModule, - FormDateTimePickerModule, ArtemisCategorySelectorModule, CustomExerciseCategoryBadgeComponent, - - ], - declarations: [ + FAQComponent, FAQUpdateComponent, - FAQComponent ], + exports: [FAQComponent, FAQUpdateComponent], }) export class ArtemisFAQModule {} From a436aa82842c57398f5612830cac35437eb25b00 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 12 Sep 2024 14:33:51 +0200 Subject: [PATCH 15/68] Removed unnecessary import statements --- src/main/webapp/app/faq/faq.module.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/main/webapp/app/faq/faq.module.ts b/src/main/webapp/app/faq/faq.module.ts index bb13f966e3b5..56765678ac1e 100644 --- a/src/main/webapp/app/faq/faq.module.ts +++ b/src/main/webapp/app/faq/faq.module.ts @@ -1,26 +1,12 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { FAQComponent } from 'app/faq/faq.component'; import { faqRoutes } from 'app/faq/faq.routes'; import { FAQUpdateComponent } from 'app/faq/faq-update.component'; -import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; -import { ArtemisCategorySelectorModule } from 'app/shared/category-selector/category-selector.module'; -import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; const ENTITY_STATES = [...faqRoutes]; @NgModule({ - imports: [ - ArtemisSharedModule, - RouterModule.forChild(ENTITY_STATES), - ArtemisSharedComponentModule, - ArtemisMarkdownEditorModule, - ArtemisCategorySelectorModule, - CustomExerciseCategoryBadgeComponent, - FAQComponent, - FAQUpdateComponent, - ], + imports: [RouterModule.forChild(ENTITY_STATES), FAQComponent, FAQUpdateComponent], exports: [FAQComponent, FAQUpdateComponent], }) export class ArtemisFAQModule {} From 117b30545eeb9c5d3acd103cc508a68ba93402a5 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Sat, 14 Sep 2024 16:12:40 +0200 Subject: [PATCH 16/68] Made filter to use badges, not plain text --- src/main/webapp/app/faq/faq.component.html | 68 +++++++++++----------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index b96feba1aa5d..578240a6ccf2 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -18,21 +18,19 @@

    - @for (category of existingCategories; track category){ -
  • - -
  • + @for (category of existingCategories; track category) { +
  • + +
  • }

@@ -50,31 +48,31 @@

- - - - - - - + + + + + + + @for (faq of filteredFaq; track trackId(i, faq); let i = $index) {
- - - - - - - - - - - -
+ + + + + + + + + + + +
- {{ faq.id }} + {{ faq.id }} {{ faq.questionTitle }} From ea49c58cab95db67d3af04b8e1494eaf6fc26fe4 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Sun, 15 Sep 2024 17:09:13 +0200 Subject: [PATCH 17/68] Added Student view and filtering as a service --- src/main/webapp/app/faq/faq.component.ts | 26 +--- src/main/webapp/app/faq/faq.service.ts | 77 +++++++----- .../course-faq-accordion-component.html | 13 ++ .../course-faq-accordion-component.scss | 18 +++ .../course-faq-accordion-component.ts | 23 ++++ .../course-faq/course-faq.component.html | 43 +++++++ .../course-faq/course-faq.component.scss | 7 ++ .../course-faq/course-faq.component.ts | 114 ++++++++++++++++++ .../app/overview/course-overview.component.ts | 24 ++++ .../app/overview/courses-routing.module.ts | 11 ++ .../webapp/i18n/en/student-dashboard.json | 3 +- 11 files changed, 304 insertions(+), 55 deletions(-) create mode 100644 src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html create mode 100644 src/main/webapp/app/overview/course-faq/course-faq-accordion-component.scss create mode 100644 src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts create mode 100644 src/main/webapp/app/overview/course-faq/course-faq.component.html create mode 100644 src/main/webapp/app/overview/course-faq/course-faq.component.scss create mode 100644 src/main/webapp/app/overview/course-faq/course-faq.component.ts diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index b942c250dc90..8d918768263d 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -86,14 +86,14 @@ export class FAQComponent implements OnInit, OnDestroy { } toggleFilters(category: string) { - if (this.activeFilters.has(category)) { - this.activeFilters.delete(category); - } else { - this.activeFilters.add(category); - } + this.activeFilters = FaqService.toggleFilter(category, this.activeFilters); this.applyFilters(); } + private applyFilters(): void { + this.filteredFaq = FaqService.applyFilters(this.activeFilters, this.faqs); + } + sortRows() { this.sortService.sortByProperty(this.filteredFaq, this.predicate, this.ascending); } @@ -116,20 +116,4 @@ export class FAQComponent implements OnInit, OnDestroy { this.existingCategories = existingCategories; }); } - - private applyFilters(): void { - if (this.activeFilters.size === 0) { - // If no filters selected, show all faqs - this.filteredFaq = this.faqs; - } else { - this.filteredFaq = this.faqs.filter((faq) => this.hasFilteredCategory(faq, this.activeFilters)); - } - } - - public hasFilteredCategory(faq: Faq, filteredCategory: Set) { - const categories = faq.categories?.map((category) => category.category); - if (categories) { - return categories.some((category) => filteredCategory.has(category!)); - } - } } diff --git a/src/main/webapp/app/faq/faq.service.ts b/src/main/webapp/app/faq/faq.service.ts index 70b6bba4fc01..5a05648723d0 100644 --- a/src/main/webapp/app/faq/faq.service.ts +++ b/src/main/webapp/app/faq/faq.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { Faq, FaqState } from 'app/entities/faq.model' +import { Faq, FaqState } from 'app/entities/faq.model'; import { Exercise } from 'app/entities/exercise.model'; import { FaqCategory } from 'app/entities/faq-category.model'; import { AlertService } from 'app/core/util/alert.service'; @@ -11,66 +11,54 @@ import { ExerciseCategory } from 'app/entities/exercise-category.model'; type EntityResponseType = HttpResponse; type EntityArrayResponseType = HttpResponse; - @Injectable({ providedIn: 'root' }) export class FaqService { - public resourceUrl = 'api/courses'; constructor( protected http: HttpClient, - protected alertService: AlertService - + protected alertService: AlertService, ) {} - create(faq: Faq): Observable{ - let copy = FaqService.convertFaqFromClient(faq) - faq.faqState = FaqState.ACCEPTED - return this.http.post( `api/faqs`,copy, { observe: 'response' }).pipe( + create(faq: Faq): Observable { + const copy = FaqService.convertFaqFromClient(faq); + faq.faqState = FaqState.ACCEPTED; + return this.http.post(`api/faqs`, copy, { observe: 'response' }).pipe( map((res: EntityResponseType) => { return res; }), ); - } - update(faq: Faq): Observable{ - let copy = FaqService.convertFaqFromClient(faq) + update(faq: Faq): Observable { + const copy = FaqService.convertFaqFromClient(faq); return this.http.put(`api/faqs/${faq.id}`, copy, { observe: 'response' }).pipe( map((res: EntityResponseType) => { return res; }), ); - } find(faqId: number): Observable { - return this.http.get(`api/faqs/${faqId}`, { observe: 'response' }).pipe( - map((res: EntityResponseType) => - FaqService.convertFaqCategoriesFromServer(res) - ), - ); + return this.http.get(`api/faqs/${faqId}`, { observe: 'response' }).pipe(map((res: EntityResponseType) => FaqService.convertFaqCategoriesFromServer(res))); } - findAllByCourseId(courseId: number): Observable { return this.http - .get(this.resourceUrl+`/${courseId}/faqs`, { + .get(this.resourceUrl + `/${courseId}/faqs`, { observe: 'response', }) - .pipe( - map((res: EntityArrayResponseType) => FaqService.convertExerciseCategoryArrayFromServer(res)) - ); + .pipe(map((res: EntityArrayResponseType) => FaqService.convertExerciseCategoryArrayFromServer(res))); } - delete(faqId: number): Observable>{ - return this.http.delete(`api/faqs/${faqId}`, { observe: 'response' }) + delete(faqId: number): Observable> { + return this.http.delete(`api/faqs/${faqId}`, { observe: 'response' }); } findAllCategoriesByCourseId(courseId: number) { - return this.http.get(this.resourceUrl+`/${courseId}/faq-categories`, { + return this.http.get(this.resourceUrl + `/${courseId}/faq-categories`, { observe: 'response', - }) + }); } /** * Converts the faq category json string into FaqCategory objects (if it exists). @@ -119,16 +107,15 @@ export class FaqService { } } - static parseFaqCategoriesString(categories?: String[]) { - let faqCategories: FaqCategory[] = [] + static parseFaqCategoriesString(categories?: string[]) { + let faqCategories: FaqCategory[] = []; if (categories) { faqCategories = categories.map((category) => { const categoryObj = JSON.parse(category as unknown as string); return new FaqCategory(categoryObj.category, categoryObj.color); }); - } - return faqCategories + return faqCategories; } /** @@ -136,14 +123,38 @@ export class FaqService { * @param { Faq } faq - faq that will be modified */ static convertFaqFromClient(faq: F): Faq { - let copy = Object.assign(faq, {}); + const copy = Object.assign(faq, {}); copy.categories = FaqService.stringifyFaqCategories(copy); if (copy.categories) { - } return copy; } + static toggleFilter(category: string, activeFilters: Set) { + if (activeFilters.has(category)) { + activeFilters.delete(category); + return activeFilters; + } else { + activeFilters.add(category); + return activeFilters; + } + } + static applyFilters(activeFilters: Set, faqs: Faq[]): Faq[] { + let filteredFaq: Faq[]; + if (activeFilters.size === 0) { + // If no filters selected, show all faqs + filteredFaq = faqs; + } else { + filteredFaq = faqs.filter((faq) => this.hasFilteredCategory(faq, activeFilters)); + } + return filteredFaq; + } + public static hasFilteredCategory(faq: Faq, filteredCategory: Set) { + const categories = faq.categories?.map((category) => category.category); + if (categories) { + return categories.some((category) => filteredCategory.has(category!)); + } + } } diff --git a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html new file mode 100644 index 000000000000..965712b9ec36 --- /dev/null +++ b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html @@ -0,0 +1,13 @@ +
+
+

{{faq().questionTitle}}

+
+ @for (category of faq().categories; track category){ + + } +
+
+
+

{{faq().questionAnswer}}

+
+
diff --git a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.scss b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.scss new file mode 100644 index 000000000000..fe52693ae074 --- /dev/null +++ b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.scss @@ -0,0 +1,18 @@ +.faq-container { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 10px; + box-sizing: border-box; +} + +.faq-container h1 { + margin: 0; +} + +.badge-container { + display: flex; + margin-left: auto; + gap: 4px; +} diff --git a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts new file mode 100644 index 000000000000..b3459c8aa980 --- /dev/null +++ b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts @@ -0,0 +1,23 @@ +import { Component, OnDestroy, input } from '@angular/core'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; +import { Faq } from 'app/entities/faq.model'; +import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; +import { Subject } from 'rxjs/internal/Subject'; + +@Component({ + selector: 'jhi-course-faq-accordion', + templateUrl: './course-faq-accordion-component.html', + styleUrl: './course-faq-accordion-component.scss', + standalone: true, + + imports: [TranslateDirective, CustomExerciseCategoryBadgeComponent], +}) +export class CourseFaqAccordionComponent implements OnDestroy { + private ngUnsubscribe = new Subject(); + faq = input.required(); + + ngOnDestroy(): void { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } +} diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.html b/src/main/webapp/app/overview/course-faq/course-faq.component.html new file mode 100644 index 000000000000..b165b4145339 --- /dev/null +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.html @@ -0,0 +1,43 @@ +
+
+ + + + +
+ +
    + @for (category of existingCategories; track category) { +
  • + +
  • + } +
+
+
+ @for (faq of this.filteredFaq; track faq) { +
+ +
+ } +
diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.scss b/src/main/webapp/app/overview/course-faq/course-faq.component.scss new file mode 100644 index 000000000000..9e1c700ded25 --- /dev/null +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.scss @@ -0,0 +1,7 @@ +.second-layer-modal-bg { + background-color: var(--secondary); +} + +.module-bg { + background-color: var(--module-bg); +} diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts new file mode 100644 index 000000000000..964ba0668b41 --- /dev/null +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -0,0 +1,114 @@ +import { Component, ElementRef, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { map } from 'rxjs/operators'; +import { Subject, Subscription } from 'rxjs'; +import { MetisService } from 'app/shared/metis/metis.service'; +import { faFilter, faPlus, faSearch, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { ButtonType } from 'app/shared/components/button.component'; +import { CourseWideSearchComponent, CourseWideSearchConfig } from 'app/overview/course-conversations/course-wide-search/course-wide-search.component'; +import { SidebarData } from 'app/types/sidebar'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { CourseFaqAccordionComponent } from 'app/overview/course-faq/course-faq-accordion-component'; +import { Faq } from 'app/entities/faq.model'; +import { FaqService } from 'app/faq/faq.service'; +import { HttpResponse } from '@angular/common/http'; +import { AlertService } from 'app/core/util/alert.service'; +import { FaqCategory } from 'app/entities/faq-category.model'; +import { loadCourseFaqCategories } from 'app/faq/faq.utils'; +import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; + +@Component({ + selector: 'jhi-course-faq', + templateUrl: './course-faq.component.html', + styleUrls: ['../course-overview.scss', './course-faq.component.scss'], + encapsulation: ViewEncapsulation.None, + providers: [MetisService], + standalone: true, + imports: [ArtemisSharedComponentModule, ArtemisSharedModule, CourseFaqAccordionComponent, CustomExerciseCategoryBadgeComponent], +}) +export class CourseFaqComponent implements OnInit, OnDestroy { + private ngUnsubscribe = new Subject(); + private parentParamSubscription: Subscription; + + courseId: number; + faqs: Faq[]; + + filteredFaq: Faq[]; + existingCategories: FaqCategory[]; + activeFilters = new Set(); + + sidebarData: SidebarData; + profileSubscription?: Subscription; + isCollapsed = false; + isProduction = true; + isTestServer = false; + + @ViewChild(CourseWideSearchComponent) + courseWideSearch: CourseWideSearchComponent; + @ViewChild('courseWideSearchInput') + searchElement: ElementRef; + + courseWideSearchConfig: CourseWideSearchConfig; + courseWideSearchTerm = ''; + readonly ButtonType = ButtonType; + + // Icons + faPlus = faPlus; + faTimes = faTimes; + faFilter = faFilter; + faSearch = faSearch; + + constructor( + private route: ActivatedRoute, + private router: Router, + private faqService: FaqService, + private alertService: AlertService, + ) {} + + ngOnInit(): void { + this.parentParamSubscription = this.route.parent!.params.subscribe((params) => { + this.courseId = Number(params.courseId); + this.loadFaqs(); + this.loadCourseExerciseCategories(this.courseId); + }); + } + + private loadCourseExerciseCategories(courseId: number) { + loadCourseFaqCategories(courseId, this.alertService, this.faqService).subscribe((existingCategories) => { + this.existingCategories = existingCategories; + }); + } + + private loadFaqs() { + this.faqService + .findAllByCourseId(this.courseId) + .pipe(map((res: HttpResponse) => res.body)) + .subscribe({ + next: (res: Faq[]) => { + this.faqs = res; + this.applyFilters(); + }, + }); + } + + ngOnDestroy() { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + this.profileSubscription?.unsubscribe(); + } + + onSearch() { + this.courseWideSearchConfig.searchTerm = this.courseWideSearchTerm; + this.courseWideSearch?.onSearch(); + } + + toggleFilters(category: string) { + this.activeFilters = FaqService.toggleFilter(category, this.activeFilters); + this.applyFilters(); + } + + private applyFilters(): void { + this.filteredFaq = FaqService.applyFilters(this.activeFilters, this.faqs); + } +} diff --git a/src/main/webapp/app/overview/course-overview.component.ts b/src/main/webapp/app/overview/course-overview.component.ts index 7cf4c805712e..282563231c76 100644 --- a/src/main/webapp/app/overview/course-overview.component.ts +++ b/src/main/webapp/app/overview/course-overview.component.ts @@ -31,6 +31,7 @@ import { faListCheck, faNetworkWired, faPersonChalkboard, + faQuestion, faSync, faTable, faTimes, @@ -171,6 +172,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit faChevronRight = faChevronRight; facSidebar = facSidebar; faEllipsis = faEllipsis; + faQuestion = faQuestion; FeatureToggle = FeatureToggle; CachingStrategy = CachingStrategy; @@ -329,6 +331,15 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit sidebarItems.push(learningPathItem); } } + + if (this.course?.faqEnabled) { + const faqItem: SidebarItem = this.getFaqItem(); + sidebarItems.push(faqItem); + if (this.course?.learningPathsEnabled) { + const learningPathItem: SidebarItem = this.getLearningPathItems(); + sidebarItems.push(learningPathItem); + } + } return sidebarItems; } @@ -437,6 +448,19 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit return dashboardItem; } + getFaqItem() { + const dashboardItem: SidebarItem = { + routerLink: 'faq', + icon: faQuestion, + title: 'Faq', + translation: 'artemisApp.courseOverview.menu.faq', + hasInOrionProperty: false, + showInOrionWindow: false, + hidden: false, + }; + return dashboardItem; + } + getDefaultItems() { const items = []; if (this.course?.studentCourseAnalyticsDashboardEnabled) { diff --git a/src/main/webapp/app/overview/courses-routing.module.ts b/src/main/webapp/app/overview/courses-routing.module.ts index 8b011de2206c..a499cd6ce483 100644 --- a/src/main/webapp/app/overview/courses-routing.module.ts +++ b/src/main/webapp/app/overview/courses-routing.module.ts @@ -12,6 +12,7 @@ import { CourseTutorialGroupDetailComponent } from './tutorial-group-details/cou import { ExamParticipationComponent } from 'app/exam/participate/exam-participation.component'; import { PendingChangesGuard } from 'app/shared/guard/pending-changes.guard'; import { CourseDashboardGuard } from 'app/overview/course-dashboard/course-dashboard-guard.service'; +import { CourseFaqComponent } from 'app/overview/course-faq/course-faq.component'; const routes: Routes = [ { @@ -255,6 +256,16 @@ const routes: Routes = [ pageTitle: 'overview.plagiarismCases', }, }, + { + path: 'faq', + component: CourseFaqComponent, + data: { + authorities: [Authority.USER], + pageTitle: 'overview.faq', + hasSidebar: true, + showRefreshButton: true, + }, + }, { path: '', redirectTo: 'dashboard', // dashboard will redirect to exercises if not enabled diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index ae6d54800cd6..3585a1303bbc 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -88,7 +88,8 @@ "testExam": "Test Exam", "communication": "Communication", "plagiarismCases": "Plagiarism Cases", - "gradingSystem": "Grading System" + "gradingSystem": "Grading System", + "faq": "Faq" }, "exerciseFilter": { "filter": "Filter", From ac619d3babac9b9def16d34668e21040864e2b60 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Mon, 16 Sep 2024 13:33:25 +0200 Subject: [PATCH 18/68] Add markdown highlighting for FAQ's --- src/main/webapp/app/faq/faq.component.html | 4 ++-- src/main/webapp/app/faq/faq.component.ts | 3 ++- .../course-faq/course-faq-accordion-component.html | 11 ++++++----- .../course-faq/course-faq-accordion-component.ts | 3 ++- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index 578240a6ccf2..cb1a1da66563 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -75,10 +75,10 @@

{{ faq.id }}

- {{ faq.questionTitle }} +

- {{ faq.questionAnswer }} +

@for (category of faq.categories; track category) { diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index 8d918768263d..1ff272c1577e 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -14,12 +14,13 @@ import { SortService } from 'app/shared/service/sort.service'; import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; @Component({ selector: 'jhi-faq', templateUrl: './faq.component.html', standalone: true, - imports: [ArtemisSharedModule, CustomExerciseCategoryBadgeComponent, ArtemisSharedComponentModule], + imports: [ArtemisSharedModule, CustomExerciseCategoryBadgeComponent, ArtemisSharedComponentModule, ArtemisMarkdownModule], }) export class FAQComponent implements OnInit, OnDestroy { faqs: Faq[]; diff --git a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html index 965712b9ec36..3e41b0eeefab 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html +++ b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html @@ -1,13 +1,14 @@ -
-
-

{{faq().questionTitle}}

+
+
+

+
@for (category of faq().categories; track category){ }
-
-

{{faq().questionAnswer}}

+
+

diff --git a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts index b3459c8aa980..08a01b290fd9 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts @@ -3,6 +3,7 @@ import { TranslateDirective } from 'app/shared/language/translate.directive'; import { Faq } from 'app/entities/faq.model'; import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; import { Subject } from 'rxjs/internal/Subject'; +import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; @Component({ selector: 'jhi-course-faq-accordion', @@ -10,7 +11,7 @@ import { Subject } from 'rxjs/internal/Subject'; styleUrl: './course-faq-accordion-component.scss', standalone: true, - imports: [TranslateDirective, CustomExerciseCategoryBadgeComponent], + imports: [TranslateDirective, CustomExerciseCategoryBadgeComponent, ArtemisMarkdownModule], }) export class CourseFaqAccordionComponent implements OnDestroy { private ngUnsubscribe = new Subject(); From b117217d4c49e43c6c2ff76bc696c61b47399a35 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 17 Sep 2024 13:17:15 +0200 Subject: [PATCH 19/68] Allowed students to pull stuff --- .../java/de/tum/in/www1/artemis/web/rest/FaqResource.java | 6 +++--- src/main/webapp/i18n/de/global.json | 3 ++- src/main/webapp/i18n/en/global.json | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java index c9b961348ca1..9deb00c3d077 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/FaqResource.java @@ -140,7 +140,7 @@ public ResponseEntity deleteFaq(@PathVariable Long faqId) { * @return the ResponseEntity with status 200 (OK) and the list of faqs in body */ @GetMapping("courses/{courseId}/faqs") - @EnforceAtLeastEditor + @EnforceAtLeastStudent public ResponseEntity> getFaqForCourse(@PathVariable Long courseId) { log.debug("REST request to get all Faqs for the course with id : {}", courseId); @@ -152,9 +152,9 @@ public ResponseEntity> getFaqForCourse(@PathVariable Long courseId) { } @GetMapping("courses/{courseId}/faq-categories") - @EnforceAtLeastEditor + @EnforceAtLeastStudent public ResponseEntity> getFaqCategoriesForCourse(@PathVariable Long courseId) { - log.debug("REST request to get all Faqs for the course with id : {}", courseId); + log.debug("REST request to get all Faq Categories for the course with id : {}", courseId); Course course = courseRepository.findByIdElseThrow(courseId); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); diff --git a/src/main/webapp/i18n/de/global.json b/src/main/webapp/i18n/de/global.json index 31fabe6283de..d5bd4ab9062b 100644 --- a/src/main/webapp/i18n/de/global.json +++ b/src/main/webapp/i18n/de/global.json @@ -346,7 +346,8 @@ "tutorialGroups": "Übungsgruppen", "statistics": "Kursstatistiken", "exams": "Klausuren", - "communication": "Kommunikation" + "communication": "Kommunikation", + "faq": "FAQ" }, "connectionStatus": { "connected": "Verbunden", diff --git a/src/main/webapp/i18n/en/global.json b/src/main/webapp/i18n/en/global.json index 18854a2515a7..ed8f81e1fbef 100644 --- a/src/main/webapp/i18n/en/global.json +++ b/src/main/webapp/i18n/en/global.json @@ -348,7 +348,8 @@ "exercises": "Exercises", "statistics": "Course statistics", "exams": "Exams", - "communication": "Communication" + "communication": "Communication", + "faq": "FAQ" }, "connectionStatus": { "connected": "Connected", From dee6e5a7f1d6da2e917fd1ce62832039d046bec4 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 3 Sep 2024 16:05:17 +0200 Subject: [PATCH 20/68] FAQ - system provisional backend --- .../tum/cit/aet/artemis/core/FaqResource.java | 154 ++++++++++++++++++ .../cit/aet/artemis/core/domain/Course.java | 30 +++- .../artemis/modeling/service/FaqService.java | 24 +++ .../aet/artemis/programming/domain/Faq.java | 82 ++++++++++ .../programming/repository/FaqRepository.java | 29 ++++ .../changelog/20240902132940_changelog.xml | 49 ++++++ .../resources/config/liquibase/master.xml | 1 + 7 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/modeling/service/FaqService.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/programming/repository/FaqRepository.java create mode 100644 src/main/resources/config/liquibase/changelog/20240902132940_changelog.xml diff --git a/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java new file mode 100644 index 000000000000..f52d0e0f8051 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java @@ -0,0 +1,154 @@ +package de.tum.in.www1.artemis.web.rest; + +import static de.tum.in.www1.artemis.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.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.Faq; +import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.repository.FaqRepository; +import de.tum.in.www1.artemis.security.Role; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastEditor; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; +import de.tum.in.www1.artemis.service.AuthorizationCheckService; +import de.tum.in.www1.artemis.service.FaqService; +import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; +import de.tum.in.www1.artemis.web.rest.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 FaqRepository faqRepository; + + private final FaqService faqService; + + private final CourseRepository courseRepository; + + private final AuthorizationCheckService authCheckService; + + public FaqResource(FaqRepository faqRepository, FaqService faqService, CourseRepository courseRepository, AuthorizationCheckService authCheckService) { + + this.faqRepository = faqRepository; + 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") + @EnforceAtLeastEditor + public ResponseEntity 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"); + } + System.out.println("Test"); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, faq.getCourse(), null); + + Faq savedFaq = faqRepository.save(faq); + return ResponseEntity.created(new URI("/api/faqs/" + savedFaq.getId())).body(savedFaq); + } + + /** + * PUT /faqs/{faqId} : Updates an existing faq. + * + * @param faq the faq to update + * @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") + @EnforceAtLeastEditor + public ResponseEntity updateFaq(@RequestBody Faq faq) { + log.debug("REST request to update Faq : {}", faq); + if (faq.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idNull"); + } + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, faq.getCourse(), null); + Faq result = faqRepository.save(faq); + return ResponseEntity.ok().body(result); + } + + /** + * 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") + @EnforceAtLeastEditor + public ResponseEntity> 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.EDITOR, course, null); + + Set faqs = faqRepository.findAllByCourseId(courseId); + + return ResponseEntity.ok().body(faqs); + } + + /** + * 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 getFaq(@PathVariable Long faqId) { + log.debug("REST request to get faq {}", faqId); + Faq faq = faqRepository.findById(faqId).orElseThrow(); + + 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 deleteFaq(@PathVariable Long faqId) { + log.debug("REST request to delete faq {}", faqId); + faqService.delete(faqId); + return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, faqId.toString())).build(); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java b/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java index beacc4af0aa0..fa3392cc7b48 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java @@ -187,6 +187,9 @@ public class Course extends DomainObject { @Column(name = "unenrollment_enabled") private boolean unenrollmentEnabled = false; + @Column(name = "faq_enabled") + private boolean faqEnabled = false; + @Column(name = "presentation_score") private Integer presentationScore; @@ -260,6 +263,10 @@ public class Course extends DomainObject { @JsonIgnoreProperties("course") private TutorialGroupsConfiguration tutorialGroupsConfiguration; + @OneToMany(mappedBy = "course", fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = "course", allowSetters = true) + private Set faqs = new HashSet<>(); + // NOTE: Helpers variable names must be different from Getter name, so that Jackson ignores the @Transient annotation, but Hibernate still respects it @Transient private Long numberOfInstructorsTransient; @@ -627,6 +634,14 @@ public void setEnrollmentEnabled(Boolean enrollmentEnabled) { this.enrollmentEnabled = enrollmentEnabled; } + public Boolean isFaqEnabled() { + return faqEnabled; + } + + public void setFaqEnabled(Boolean faqEnabled) { + this.faqEnabled = faqEnabled; + } + public String getEnrollmentConfirmationMessage() { return enrollmentConfirmationMessage; } @@ -717,7 +732,7 @@ public String toString() { + "'" + ", enrollmentStartDate='" + getEnrollmentStartDate() + "'" + ", enrollmentEndDate='" + getEnrollmentEndDate() + "'" + ", unenrollmentEndDate='" + getUnenrollmentEndDate() + "'" + ", semester='" + getSemester() + "'" + "'" + ", onlineCourse='" + isOnlineCourse() + "'" + ", color='" + getColor() + "'" + ", courseIcon='" + getCourseIcon() + "'" + ", enrollmentEnabled='" + isEnrollmentEnabled() + "'" + ", unenrollmentEnabled='" + isUnenrollmentEnabled() + "'" - + ", presentationScore='" + getPresentationScore() + "'" + "}"; + + ", presentationScore='" + getPresentationScore() + ", faqEnabled='" + isFaqEnabled() + "'" + "}"; } public void setNumberOfInstructors(Long numberOfInstructors) { @@ -1057,4 +1072,17 @@ public String getMappedColumnName() { return mappedColumnName; } } + + public Set getFaqs() { + return faqs; + } + + public void setFaqs(Set faqs) { + this.faqs = faqs; + } + + public void addFaq(Faq faq) { + this.faqs.add(faq); + faq.setCourse(this); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/modeling/service/FaqService.java b/src/main/java/de/tum/cit/aet/artemis/modeling/service/FaqService.java new file mode 100644 index 000000000000..d1e762fcf679 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/modeling/service/FaqService.java @@ -0,0 +1,24 @@ +package de.tum.in.www1.artemis.service; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +@Profile(PROFILE_CORE) +@Service +public class FaqService { + + public FaqService() { + + } + + /** + * Deletes the given lecture (with its lecture units). + * + * @param faqId the faqId of to be deleted faq + */ + public void delete(long faqId) { + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java new file mode 100644 index 000000000000..fd6225523e08 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java @@ -0,0 +1,82 @@ +package de.tum.in.www1.artemis.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.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; + +/** + * A FAQ. + */ +@Entity +@Table(name = "faq") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class Faq extends DomainObject { + + @Column(name = "question_title") + private String questionTitle; + + @Column(name = "question_answer") + private String questionAnswer; + + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "faq_categories", joinColumns = @JoinColumn(name = "faq_id")) + @Column(name = "categories") + private Set categories = new HashSet<>(); + + @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 getCategories() { + return categories; + } + + public void setCategories(Set categories) { + this.categories = categories; + } + + @Override + public String toString() { + return "Faq{" + "id=" + getId() + ", title='" + getQuestionTitle() + "'" + ", description='" + getQuestionTitle() + "'" + "}"; + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/FaqRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/FaqRepository.java new file mode 100644 index 000000000000..84bbb7ff50da --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/FaqRepository.java @@ -0,0 +1,29 @@ +package de.tum.in.www1.artemis.repository; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; + +import java.util.Set; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import de.tum.in.www1.artemis.domain.Faq; +import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; + +/** + * Spring Data repository for the Faq entity. + */ +@Profile(PROFILE_CORE) +@Repository +public interface FaqRepository extends ArtemisJpaRepository { + + @Query(""" + SELECT faq + FROM Faq faq + WHERE faq.course.id = :courseId + """) + Set findAllByCourseId(@Param("courseId") Long courseId); + +} diff --git a/src/main/resources/config/liquibase/changelog/20240902132940_changelog.xml b/src/main/resources/config/liquibase/changelog/20240902132940_changelog.xml new file mode 100644 index 000000000000..8ea56581c20e --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240902132940_changelog.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index f8be6b6255a0..98da565c5c8d 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -22,6 +22,7 @@ + From 8d254d26c2857d22e0e45e320b3bc94ef3937608 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 3 Sep 2024 17:52:41 +0200 Subject: [PATCH 21/68] Add meta information and state to FAQ --- .../tum/cit/aet/artemis/core/FaqResource.java | 2 +- .../aet/artemis/programming/domain/Faq.java | 18 ++++++++++++++++-- .../artemis/programming/domain/FaqState.java | 5 +++++ ...ngelog.xml => 20240902175045_changelog.xml} | 15 ++++++++++++--- src/main/resources/config/liquibase/master.xml | 2 +- 5 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/programming/domain/FaqState.java rename src/main/resources/config/liquibase/changelog/{20240902132940_changelog.xml => 20240902175045_changelog.xml} (78%) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java index f52d0e0f8051..0b779164e0fa 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java @@ -92,7 +92,7 @@ public ResponseEntity createFaq(@RequestBody Faq faq) throws URISyntaxExcep * @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") + @PutMapping("faqs/{faqId}") @EnforceAtLeastEditor public ResponseEntity updateFaq(@RequestBody Faq faq) { log.debug("REST request to update Faq : {}", faq); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java index fd6225523e08..98746c97fab9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java @@ -7,6 +7,8 @@ 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; @@ -25,7 +27,7 @@ @Table(name = "faq") @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @JsonInclude(JsonInclude.Include.NON_EMPTY) -public class Faq extends DomainObject { +public class Faq extends AbstractAuditingEntity { @Column(name = "question_title") private String questionTitle; @@ -38,6 +40,10 @@ public class Faq extends DomainObject { @Column(name = "categories") private Set categories = new HashSet<>(); + @Enumerated(EnumType.STRING) + @Column(name = "faq_state") + private FaqState faqState; + @ManyToOne @JsonIgnoreProperties(value = { "faqs" }, allowSetters = true) private Course course; @@ -74,9 +80,17 @@ public void setCategories(Set 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() + "'" + "}"; + return "Faq{" + "id=" + getId() + ", title='" + getQuestionTitle() + "'" + ", description='" + getQuestionTitle() + "'" + ", faqState='" + getFaqState() + "}"; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/FaqState.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/FaqState.java new file mode 100644 index 000000000000..7ba46b7dddb5 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/FaqState.java @@ -0,0 +1,5 @@ +package de.tum.in.www1.artemis.domain; + +public enum FaqState { + ACCEPTED, REJECTED, PROPOSED +} diff --git a/src/main/resources/config/liquibase/changelog/20240902132940_changelog.xml b/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml similarity index 78% rename from src/main/resources/config/liquibase/changelog/20240902132940_changelog.xml rename to src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml index 8ea56581c20e..56c360204fc2 100644 --- a/src/main/resources/config/liquibase/changelog/20240902132940_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml @@ -7,21 +7,30 @@ - + - + + + + + + + + + + - + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 98da565c5c8d..69af70b96f56 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -22,7 +22,7 @@ - + From d1f782392be44bc5f6d80edff6c5b9db3dcedd17 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 3 Sep 2024 18:08:07 +0200 Subject: [PATCH 22/68] Fixed minor mapping error --- .../java/de/tum/cit/aet/artemis/programming/domain/Faq.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java index 98746c97fab9..8b1e12287d96 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java @@ -37,7 +37,7 @@ public class Faq extends AbstractAuditingEntity { @ElementCollection(fetch = FetchType.LAZY) @CollectionTable(name = "faq_categories", joinColumns = @JoinColumn(name = "faq_id")) - @Column(name = "categories") + @Column(name = "category") private Set categories = new HashSet<>(); @Enumerated(EnumType.STRING) From af53c204fc10e03725bf5574982868be451a74c4 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 5 Sep 2024 15:05:53 +0200 Subject: [PATCH 23/68] Added cascade delete --- src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java b/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java index fa3392cc7b48..39ab15962fe7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java @@ -263,7 +263,7 @@ public class Course extends DomainObject { @JsonIgnoreProperties("course") private TutorialGroupsConfiguration tutorialGroupsConfiguration; - @OneToMany(mappedBy = "course", fetch = FetchType.LAZY) + @OneToMany(mappedBy = "course", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) @JsonIgnoreProperties(value = "course", allowSetters = true) private Set faqs = new HashSet<>(); From a5b77935f68800b101ddc625aa6fd46f015d3746 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Mon, 9 Sep 2024 13:09:43 +0200 Subject: [PATCH 24/68] Added cascade deletion on course deletion --- .../tum/cit/aet/artemis/core/FaqResource.java | 58 ++++--- .../artemis/core/service/CourseService.java | 143 +++++++++--------- .../artemis/modeling/service/FaqService.java | 10 +- .../aet/artemis/programming/domain/Faq.java | 4 +- .../programming/repository/FaqRepository.java | 18 +++ 5 files changed, 137 insertions(+), 96 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java index 0b779164e0fa..21ad760776cd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java @@ -78,7 +78,6 @@ public ResponseEntity createFaq(@RequestBody Faq faq) throws URISyntaxExcep if (faq.getId() != null) { throw new BadRequestAlertException("A new faq cannot already have an ID", ENTITY_NAME, "idExists"); } - System.out.println("Test"); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, faq.getCourse(), null); Faq savedFaq = faqRepository.save(faq); @@ -104,25 +103,6 @@ public ResponseEntity updateFaq(@RequestBody Faq faq) { return ResponseEntity.ok().body(result); } - /** - * 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") - @EnforceAtLeastEditor - public ResponseEntity> 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.EDITOR, course, null); - - Set faqs = faqRepository.findAllByCourseId(courseId); - - return ResponseEntity.ok().body(faqs); - } - /** * GET /faqs/:faqId : get the "faqId" faq. * @@ -134,7 +114,7 @@ public ResponseEntity> getFaqForCourse(@PathVariable Long courseId) { public ResponseEntity getFaq(@PathVariable Long faqId) { log.debug("REST request to get faq {}", faqId); Faq faq = faqRepository.findById(faqId).orElseThrow(); - + System.out.println(faq.getCategories()); return ResponseEntity.ok(faq); } @@ -147,8 +127,42 @@ public ResponseEntity getFaq(@PathVariable Long faqId) { @DeleteMapping("faqs/{faqId}") @EnforceAtLeastInstructor public ResponseEntity deleteFaq(@PathVariable Long faqId) { + log.debug("REST request to delete faq {}", faqId); - faqService.delete(faqId); + 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") + @EnforceAtLeastEditor + public ResponseEntity> 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.EDITOR, course, null); + + Set faqs = faqRepository.findAllByCourseId(courseId); + return ResponseEntity.ok().body(faqs); + } + + @GetMapping("courses/{courseId}/faq-categories") + @EnforceAtLeastEditor + public ResponseEntity> getFaqCategoriesForCourse(@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.EDITOR, course, null); + + Set faqs = faqRepository.findAllCategoriesByCourseId(courseId); + + return ResponseEntity.ok().body(faqs); + } + } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index 9e3b69f269cc..a8395bb2c486 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java @@ -1,9 +1,9 @@ -package de.tum.cit.aet.artemis.core.service; +package de.tum.in.www1.artemis.service; -import static de.tum.cit.aet.artemis.assessment.domain.ComplaintType.COMPLAINT; -import static de.tum.cit.aet.artemis.assessment.domain.ComplaintType.MORE_FEEDBACK; -import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; -import static de.tum.cit.aet.artemis.core.util.RoundingUtil.roundScoreSpecifiedByCourseSettings; +import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; +import static de.tum.in.www1.artemis.domain.enumeration.ComplaintType.COMPLAINT; +import static de.tum.in.www1.artemis.domain.enumeration.ComplaintType.MORE_FEEDBACK; +import static de.tum.in.www1.artemis.service.util.RoundingUtil.roundScoreSpecifiedByCourseSettings; import java.nio.file.Files; import java.nio.file.Path; @@ -43,70 +43,65 @@ import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; -import de.tum.cit.aet.artemis.assessment.domain.GradingScale; -import de.tum.cit.aet.artemis.assessment.repository.ComplaintRepository; -import de.tum.cit.aet.artemis.assessment.repository.ComplaintResponseRepository; -import de.tum.cit.aet.artemis.assessment.repository.GradingScaleRepository; -import de.tum.cit.aet.artemis.assessment.repository.ParticipantScoreRepository; -import de.tum.cit.aet.artemis.assessment.repository.RatingRepository; -import de.tum.cit.aet.artemis.assessment.repository.ResultRepository; -import de.tum.cit.aet.artemis.assessment.service.ComplaintService; -import de.tum.cit.aet.artemis.assessment.service.PresentationPointsCalculationService; -import de.tum.cit.aet.artemis.assessment.service.TutorLeaderboardService; -import de.tum.cit.aet.artemis.atlas.repository.CompetencyRelationRepository; -import de.tum.cit.aet.artemis.atlas.repository.CompetencyRepository; -import de.tum.cit.aet.artemis.atlas.repository.PrerequisiteRepository; -import de.tum.cit.aet.artemis.atlas.service.learningpath.LearningPathService; -import de.tum.cit.aet.artemis.communication.domain.NotificationType; -import de.tum.cit.aet.artemis.communication.domain.notification.GroupNotification; -import de.tum.cit.aet.artemis.communication.repository.GroupNotificationRepository; -import de.tum.cit.aet.artemis.communication.repository.conversation.ConversationRepository; -import de.tum.cit.aet.artemis.communication.service.notifications.GroupNotificationService; -import de.tum.cit.aet.artemis.core.config.Constants; -import de.tum.cit.aet.artemis.core.domain.Course; -import de.tum.cit.aet.artemis.core.domain.DomainObject; -import de.tum.cit.aet.artemis.core.domain.User; -import de.tum.cit.aet.artemis.core.dto.CourseContentCount; -import de.tum.cit.aet.artemis.core.dto.CourseManagementDetailViewDTO; -import de.tum.cit.aet.artemis.core.dto.DueDateStat; -import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; -import de.tum.cit.aet.artemis.core.dto.StatisticsEntry; -import de.tum.cit.aet.artemis.core.dto.StatsForDashboardDTO; -import de.tum.cit.aet.artemis.core.dto.StudentDTO; -import de.tum.cit.aet.artemis.core.dto.TutorLeaderboardDTO; -import de.tum.cit.aet.artemis.core.dto.pageablesearch.SearchTermPageableSearchDTO; -import de.tum.cit.aet.artemis.core.repository.CourseRepository; -import de.tum.cit.aet.artemis.core.repository.StatisticsRepository; -import de.tum.cit.aet.artemis.core.repository.UserRepository; -import de.tum.cit.aet.artemis.core.security.Role; -import de.tum.cit.aet.artemis.core.security.SecurityUtils; -import de.tum.cit.aet.artemis.core.service.export.CourseExamExportService; -import de.tum.cit.aet.artemis.core.service.user.UserService; -import de.tum.cit.aet.artemis.core.util.PageUtil; -import de.tum.cit.aet.artemis.core.util.TimeLogUtil; -import de.tum.cit.aet.artemis.exam.domain.Exam; -import de.tum.cit.aet.artemis.exam.domain.ExerciseGroup; -import de.tum.cit.aet.artemis.exam.repository.ExamRepository; -import de.tum.cit.aet.artemis.exam.repository.ExerciseGroupRepository; -import de.tum.cit.aet.artemis.exam.service.ExamDeletionService; -import de.tum.cit.aet.artemis.exercise.domain.Exercise; -import de.tum.cit.aet.artemis.exercise.domain.IncludedInOverallScore; -import de.tum.cit.aet.artemis.exercise.repository.ExerciseRepository; -import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository; -import de.tum.cit.aet.artemis.exercise.repository.SubmissionRepository; -import de.tum.cit.aet.artemis.exercise.service.ExerciseDeletionService; -import de.tum.cit.aet.artemis.exercise.service.ExerciseService; -import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService; -import de.tum.cit.aet.artemis.lecture.domain.Lecture; -import de.tum.cit.aet.artemis.lecture.repository.LectureRepository; -import de.tum.cit.aet.artemis.lecture.service.LectureService; -import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismCase; -import de.tum.cit.aet.artemis.plagiarism.repository.PlagiarismCaseRepository; -import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; -import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; -import de.tum.cit.aet.artemis.tutorialgroup.repository.TutorialGroupNotificationRepository; -import de.tum.cit.aet.artemis.tutorialgroup.repository.TutorialGroupRepository; -import de.tum.cit.aet.artemis.tutorialgroup.service.TutorialGroupChannelManagementService; +import de.tum.in.www1.artemis.config.Constants; +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.DomainObject; +import de.tum.in.www1.artemis.domain.Exercise; +import de.tum.in.www1.artemis.domain.GradingScale; +import de.tum.in.www1.artemis.domain.Lecture; +import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; +import de.tum.in.www1.artemis.domain.enumeration.NotificationType; +import de.tum.in.www1.artemis.domain.exam.Exam; +import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; +import de.tum.in.www1.artemis.domain.notification.GroupNotification; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismCase; +import de.tum.in.www1.artemis.domain.statistics.StatisticsEntry; +import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; +import de.tum.in.www1.artemis.repository.CompetencyRepository; +import de.tum.in.www1.artemis.repository.ComplaintRepository; +import de.tum.in.www1.artemis.repository.ComplaintResponseRepository; +import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.repository.ExamRepository; +import de.tum.in.www1.artemis.repository.ExerciseGroupRepository; +import de.tum.in.www1.artemis.repository.ExerciseRepository; +import de.tum.in.www1.artemis.repository.FaqRepository; +import de.tum.in.www1.artemis.repository.GradingScaleRepository; +import de.tum.in.www1.artemis.repository.GroupNotificationRepository; +import de.tum.in.www1.artemis.repository.LectureRepository; +import de.tum.in.www1.artemis.repository.ParticipantScoreRepository; +import de.tum.in.www1.artemis.repository.PrerequisiteRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; +import de.tum.in.www1.artemis.repository.RatingRepository; +import de.tum.in.www1.artemis.repository.ResultRepository; +import de.tum.in.www1.artemis.repository.StatisticsRepository; +import de.tum.in.www1.artemis.repository.StudentParticipationRepository; +import de.tum.in.www1.artemis.repository.SubmissionRepository; +import de.tum.in.www1.artemis.repository.UserRepository; +import de.tum.in.www1.artemis.repository.metis.conversation.ConversationRepository; +import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismCaseRepository; +import de.tum.in.www1.artemis.repository.tutorialgroups.TutorialGroupNotificationRepository; +import de.tum.in.www1.artemis.repository.tutorialgroups.TutorialGroupRepository; +import de.tum.in.www1.artemis.security.Role; +import de.tum.in.www1.artemis.security.SecurityUtils; +import de.tum.in.www1.artemis.service.dto.StudentDTO; +import de.tum.in.www1.artemis.service.exam.ExamDeletionService; +import de.tum.in.www1.artemis.service.export.CourseExamExportService; +import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; +import de.tum.in.www1.artemis.service.learningpath.LearningPathService; +import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; +import de.tum.in.www1.artemis.service.tutorialgroups.TutorialGroupChannelManagementService; +import de.tum.in.www1.artemis.service.user.UserService; +import de.tum.in.www1.artemis.service.util.TimeLogUtil; +import de.tum.in.www1.artemis.web.rest.dto.CourseContentCount; +import de.tum.in.www1.artemis.web.rest.dto.CourseManagementDetailViewDTO; +import de.tum.in.www1.artemis.web.rest.dto.DueDateStat; +import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; +import de.tum.in.www1.artemis.web.rest.dto.StatsForDashboardDTO; +import de.tum.in.www1.artemis.web.rest.dto.TutorLeaderboardDTO; +import de.tum.in.www1.artemis.web.rest.dto.pageablesearch.SearchTermPageableSearchDTO; +import de.tum.in.www1.artemis.web.rest.util.PageUtil; /** * Service Implementation for managing Course. @@ -117,6 +112,8 @@ public class CourseService { private static final Logger log = LoggerFactory.getLogger(CourseService.class); + private final FaqRepository faqRepository; + @Value("${artemis.course-archives-path}") private Path courseArchivesDirPath; @@ -210,7 +207,7 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise TutorialGroupRepository tutorialGroupRepository, PlagiarismCaseRepository plagiarismCaseRepository, ConversationRepository conversationRepository, LearningPathService learningPathService, Optional irisSettingsService, LectureRepository lectureRepository, TutorialGroupNotificationRepository tutorialGroupNotificationRepository, TutorialGroupChannelManagementService tutorialGroupChannelManagementService, - PrerequisiteRepository prerequisiteRepository, CompetencyRelationRepository competencyRelationRepository) { + PrerequisiteRepository prerequisiteRepository, CompetencyRelationRepository competencyRelationRepository, FaqRepository faqRepository) { this.courseRepository = courseRepository; this.exerciseService = exerciseService; this.exerciseDeletionService = exerciseDeletionService; @@ -250,6 +247,7 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise this.tutorialGroupChannelManagementService = tutorialGroupChannelManagementService; this.prerequisiteRepository = prerequisiteRepository; this.competencyRelationRepository = competencyRelationRepository; + this.faqRepository = faqRepository; } /** @@ -467,6 +465,7 @@ public void delete(Course course) { deleteDefaultGroups(course); deleteExamsOfCourse(course); deleteGradingScaleOfCourse(course); + deleteFaqOfCourse(course); irisSettingsService.ifPresent(iss -> iss.deleteSettingsFor(course)); courseRepository.deleteById(course.getId()); log.debug("Successfully deleted course {}.", course.getTitle()); @@ -542,6 +541,10 @@ private void deleteCompetenciesOfCourse(Course course) { competencyRepository.deleteAll(course.getCompetencies()); } + private void deleteFaqOfCourse(Course course) { + faqRepository.deleteAllByCourseId(course.getId()); + } + /** * If the exercise is part of an exam, retrieve the course through ExerciseGroup -> Exam -> Course. * Otherwise, the course is already set and the id can be used to retrieve the course from the database. diff --git a/src/main/java/de/tum/cit/aet/artemis/modeling/service/FaqService.java b/src/main/java/de/tum/cit/aet/artemis/modeling/service/FaqService.java index d1e762fcf679..0f213a1fc8e2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/modeling/service/FaqService.java +++ b/src/main/java/de/tum/cit/aet/artemis/modeling/service/FaqService.java @@ -5,12 +5,16 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import de.tum.in.www1.artemis.repository.FaqRepository; + @Profile(PROFILE_CORE) @Service public class FaqService { - public FaqService() { + private final FaqRepository faqRepository; + public FaqService(FaqRepository faqRepository) { + this.faqRepository = faqRepository; } /** @@ -18,7 +22,9 @@ public FaqService() { * * @param faqId the faqId of to be deleted faq */ - public void delete(long faqId) { + public void deleteById(long faqId) { + faqRepository.deleteById(faqId); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java index 8b1e12287d96..61bb6f525526 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java @@ -35,9 +35,9 @@ public class Faq extends AbstractAuditingEntity { @Column(name = "question_answer") private String questionAnswer; - @ElementCollection(fetch = FetchType.LAZY) + @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "faq_categories", joinColumns = @JoinColumn(name = "faq_id")) - @Column(name = "category") + @Column(name = "categories") private Set categories = new HashSet<>(); @Enumerated(EnumType.STRING) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/FaqRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/FaqRepository.java index 84bbb7ff50da..8e04d87fdb42 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/FaqRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/FaqRepository.java @@ -5,9 +5,11 @@ 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.in.www1.artemis.domain.Faq; import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; @@ -26,4 +28,20 @@ public interface FaqRepository extends ArtemisJpaRepository { """) Set findAllByCourseId(@Param("courseId") Long courseId); + @Query(""" + SELECT distinct faq.categories + FROM Faq faq + WHERE faq.course.id = :courseId + """) + Set findAllCategoriesByCourseId(@Param("courseId") Long courseId); + + @Transactional + @Modifying + @Query(""" + DELETE + FROM Faq faq + WHERE faq.course.id = :courseId + """) + void deleteAllByCourseId(@Param("courseId") Long courseId); + } From 58136b6de70ed86abe77770bb4fe4a94aca99040 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 10 Sep 2024 10:35:51 +0200 Subject: [PATCH 25/68] Changed rest of server stuff --- src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java | 1 - .../config/liquibase/changelog/20240902175045_changelog.xml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java index 21ad760776cd..c9b961348ca1 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java @@ -114,7 +114,6 @@ public ResponseEntity updateFaq(@RequestBody Faq faq) { public ResponseEntity getFaq(@PathVariable Long faqId) { log.debug("REST request to get faq {}", faqId); Faq faq = faqRepository.findById(faqId).orElseThrow(); - System.out.println(faq.getCategories()); return ResponseEntity.ok(faq); } diff --git a/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml b/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml index 56c360204fc2..3f2400598da3 100644 --- a/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml @@ -30,7 +30,7 @@ - + From db67131757522f18689a038a48485591afdcc6b6 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 10 Sep 2024 11:37:31 +0200 Subject: [PATCH 26/68] Add translations and fix uppercase --- .../programming/repository/FaqRepository.java | 2 +- src/main/webapp/i18n/de/course.json | 4 +++ src/main/webapp/i18n/de/faq.json | 26 +++++++++++++++++++ src/main/webapp/i18n/de/global.json | 3 ++- src/main/webapp/i18n/en/course.json | 4 +++ src/main/webapp/i18n/en/faq.json | 26 +++++++++++++++++++ src/main/webapp/i18n/en/global.json | 3 ++- 7 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 src/main/webapp/i18n/de/faq.json create mode 100644 src/main/webapp/i18n/en/faq.json diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/FaqRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/FaqRepository.java index 8e04d87fdb42..dd36a4940187 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/FaqRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/FaqRepository.java @@ -29,7 +29,7 @@ public interface FaqRepository extends ArtemisJpaRepository { Set findAllByCourseId(@Param("courseId") Long courseId); @Query(""" - SELECT distinct faq.categories + SELECT DISTINCT faq.categories FROM Faq faq WHERE faq.course.id = :courseId """) diff --git a/src/main/webapp/i18n/de/course.json b/src/main/webapp/i18n/de/course.json index 7bc9274e7461..cdcf9ffdd322 100644 --- a/src/main/webapp/i18n/de/course.json +++ b/src/main/webapp/i18n/de/course.json @@ -106,6 +106,10 @@ "label": "Direktnachrichten / Gruppen-Chats aktiviert", "tooltip": "Ermöglicht den Nachrichtenaustausch in Gruppenchats oder Direktnachrichten. Alle Nutzer:innen können Direktnachrichten oder einen privaten Gruppenchat starten und andere Nutzer:innen hinzufügen. Ein Gruppenchat ist auf zehn Mitglieder:innen begrenzt. Die Chats finden im Kommunikationbereich des Kurses statt.", "codeOfConduct": "Nachrichten: Code of Conduct" + }, + "faqEnabled": { + "label": "FAQ aktivieren", + "tooltip": "Ermöglicht das Erstellen von FAQ-Einträgen, in denen Lehrende häufig gestellte Fragen sammeln. Studierende können auf diese Wissenssammlung zugreifen, um eigenständig nachzuarbeiten und ihre Fragen zu klären." } }, "enrollmentEnabled": { diff --git a/src/main/webapp/i18n/de/faq.json b/src/main/webapp/i18n/de/faq.json new file mode 100644 index 000000000000..585a84a147d3 --- /dev/null +++ b/src/main/webapp/i18n/de/faq.json @@ -0,0 +1,26 @@ +{ + "artemisApp": { + "faq": { + "home": { + "title": "FAQ", + "createLabel": "FAQ erstellen", + "filterLabel": "Filter", + "createOrEditLabel": "FAQ erstellen oder bearbeiten" + }, + "created": "FAQ erstellt mit ID {{ param }}", + "updated": "FAQ aktualisiert mit ID {{ param }}", + "deleted": "FAQ gelöscht mit ID {{ param }}", + "delete": { + "question": "Soll das FAQ {{ title }} wirklich dauerhaft gelöscht werden? Diese Aktion kann NICHT rückgängig gemacht werden!", + "typeNameToConfirm": "Bitte gib den Namen des FAQ zur Bestätigung ein." + }, + + "table": { + "questionTitle": "Fragentitel", + "questionAnswer": "Antwort auf die Frage", + "categories": "Kategorien" + }, + "course": "Kurs" + } + } +} diff --git a/src/main/webapp/i18n/de/global.json b/src/main/webapp/i18n/de/global.json index 9db7bb972f26..31fabe6283de 100644 --- a/src/main/webapp/i18n/de/global.json +++ b/src/main/webapp/i18n/de/global.json @@ -266,7 +266,8 @@ "goBack": "Zurück", "search": "Suchen", "select": "Auswählen", - "sendToIris": "An Iris schicken" + "sendToIris": "An Iris schicken", + "faq": "FAQ" }, "detail": { "field": "Feld", diff --git a/src/main/webapp/i18n/en/course.json b/src/main/webapp/i18n/en/course.json index e977ca0d3f3f..8fee816f7faf 100644 --- a/src/main/webapp/i18n/en/course.json +++ b/src/main/webapp/i18n/en/course.json @@ -106,6 +106,10 @@ "label": "Direct Messages / Group Chats Enabled", "tooltip": "Enables messaging between course users in group chats or direct messages. Every user can start a direct message, private group chat and add other users. A group chat is limited to 10 members. The chats happens in the communication space of the course.", "codeOfConduct": "Messaging Code of Conduct" + }, + "faqEnabled": { + "label": "FAQ Enabled", + "tooltip": "Enables the creation of FAQ entries where instructors can collect frequently asked questions. Students can access this knowledge base to review independently and clarify their questions." } }, "enrollmentEnabled": { diff --git a/src/main/webapp/i18n/en/faq.json b/src/main/webapp/i18n/en/faq.json new file mode 100644 index 000000000000..26567d8c3927 --- /dev/null +++ b/src/main/webapp/i18n/en/faq.json @@ -0,0 +1,26 @@ +{ + "artemisApp": { + "faq": { + "home": { + "title": "FAQ", + "createLabel": "Create a new FAQ", + "filterLabel": "Filter", + "createOrEditLabel": "FAQ erstellen oder bearbeiten" + }, + "created": "Created new FAQ with identifier {{ param }}", + "updated": "Updated FAQ with identifier {{ param }}", + "deleted": "Deleted FAQ with identifier {{ param }}", + "delete": { + "question": "Are you sure you want to permanently delete the FAQ {{ title }}? This action can NOT be undone!", + "typeNameToConfirm": "Please type in the name of the FAQ to confirm." + }, + + "table": { + "questionTitle": "Question title", + "questionAnswer": "Question answer", + "categories": "Categories" + }, + "course": "Course" + } + } +} diff --git a/src/main/webapp/i18n/en/global.json b/src/main/webapp/i18n/en/global.json index 8760c0192b62..18854a2515a7 100644 --- a/src/main/webapp/i18n/en/global.json +++ b/src/main/webapp/i18n/en/global.json @@ -268,7 +268,8 @@ "goBack": "Go back", "search": "Search", "select": "Select", - "sendToIris": "Send To Iris" + "sendToIris": "Send To Iris", + "faq": "FAQ" }, "detail": { "field": "Field", From 6c494e73267f52ef67aa9aa709f5b8bdfef7fb84 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 10 Sep 2024 19:49:22 +0200 Subject: [PATCH 27/68] Add first draft of FAQ System --- .../course-management-tab-bar.component.html | 6 + .../course-management-tab-bar.component.ts | 2 + .../course/manage/course-management.module.ts | 2 + .../manage/course-update.component.html | 20 +++ .../course/manage/course-update.component.ts | 8 +- .../course-management-card.component.html | 12 ++ .../course-management-card.component.ts | 2 + src/main/webapp/app/entities/course.model.ts | 1 + .../webapp/app/entities/faq-category.model.ts | 29 ++++ src/main/webapp/app/entities/faq.model.ts | 24 +++ .../webapp/app/faq/faq-update.component.html | 52 ++++++ .../webapp/app/faq/faq-update.component.scss | 7 + .../webapp/app/faq/faq-update.component.ts | 154 ++++++++++++++++++ src/main/webapp/app/faq/faq.component.html | 123 ++++++++++++++ src/main/webapp/app/faq/faq.component.ts | 144 ++++++++++++++++ src/main/webapp/app/faq/faq.module.ts | 34 ++++ src/main/webapp/app/faq/faq.routes.ts | 89 ++++++++++ src/main/webapp/app/faq/faq.service.ts | 149 +++++++++++++++++ src/main/webapp/app/faq/faq.utils.ts | 33 ++++ .../category-selector.component.ts | 5 +- ...ustom-exercise-category-badge.component.ts | 3 +- 21 files changed, 895 insertions(+), 4 deletions(-) create mode 100644 src/main/webapp/app/entities/faq-category.model.ts create mode 100644 src/main/webapp/app/entities/faq.model.ts create mode 100644 src/main/webapp/app/faq/faq-update.component.html create mode 100644 src/main/webapp/app/faq/faq-update.component.scss create mode 100644 src/main/webapp/app/faq/faq-update.component.ts create mode 100644 src/main/webapp/app/faq/faq.component.html create mode 100644 src/main/webapp/app/faq/faq.component.ts create mode 100644 src/main/webapp/app/faq/faq.module.ts create mode 100644 src/main/webapp/app/faq/faq.routes.ts create mode 100644 src/main/webapp/app/faq/faq.service.ts create mode 100644 src/main/webapp/app/faq/faq.utils.ts diff --git a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html index 3c953bcf4e3a..abbfdaf7c010 100644 --- a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html +++ b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html @@ -72,6 +72,12 @@ } + @if (course.isAtLeastInstructor && course.faqEnabled) { + + + + + } @if (course.isAtLeastInstructor && localCIActive) { diff --git a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts index c46c04bbcf5d..bf4cf8452999 100644 --- a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts +++ b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts @@ -25,6 +25,7 @@ import { faTrash, faUserCheck, faWrench, + faQuestion } from '@fortawesome/free-solid-svg-icons'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; import { CourseAdminService } from 'app/course/manage/course-admin.service'; @@ -73,6 +74,7 @@ export class CourseManagementTabBarComponent implements OnInit, OnDestroy, After faRobot = faRobot; faPuzzlePiece = faPuzzlePiece; faList = faList; + faQuestion = faQuestion isCommunicationEnabled = false; diff --git a/src/main/webapp/app/course/manage/course-management.module.ts b/src/main/webapp/app/course/manage/course-management.module.ts index 333de23de8b3..22cfc65562a8 100644 --- a/src/main/webapp/app/course/manage/course-management.module.ts +++ b/src/main/webapp/app/course/manage/course-management.module.ts @@ -70,6 +70,7 @@ import { SubmissionResultStatusModule } from 'app/overview/submission-result-sta import { ImageCropperModalComponent } from 'app/course/manage/image-cropper-modal.component'; import { HeaderCourseComponent } from 'app/overview/header-course.component'; import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; +import { ArtemisFAQModule } from 'app/faq/faq.module'; @NgModule({ imports: [ @@ -124,6 +125,7 @@ import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown DetailModule, SubmissionResultStatusModule, ArtemisMarkdownEditorModule, + ArtemisFAQModule, ], declarations: [ CourseManagementComponent, diff --git a/src/main/webapp/app/course/manage/course-update.component.html b/src/main/webapp/app/course/manage/course-update.component.html index 43b03f3f9040..4906832adf6c 100644 --- a/src/main/webapp/app/course/manage/course-update.component.html +++ b/src/main/webapp/app/course/manage/course-update.component.html @@ -347,6 +347,26 @@
ngbTooltip="{{ 'artemisApp.course.courseCommunicationSetting.messagingEnabled.tooltip' | artemisTranslate }}" />
+
+ + + +
@if (communicationEnabled) {
diff --git a/src/main/webapp/app/course/manage/course-update.component.ts b/src/main/webapp/app/course/manage/course-update.component.ts index e60f4e949eb6..69549695b862 100644 --- a/src/main/webapp/app/course/manage/course-update.component.ts +++ b/src/main/webapp/app/course/manage/course-update.component.ts @@ -71,6 +71,7 @@ export class CourseUpdateComponent implements OnInit { faExclamationTriangle = faExclamationTriangle; faPen = faPen; + faqEnabled = true communicationEnabled = true; messagingEnabled = true; ltiEnabled = false; @@ -115,7 +116,7 @@ export class CourseUpdateComponent implements OnInit { this.courseOrganizations = organizations; }); this.originalTimeZone = this.course.timeZone; - + this.faqEnabled = course.faqEnabled // complaints are only enabled when at least one complaint is allowed and the complaint duration is positive this.complaintsEnabled = (this.course.maxComplaints! > 0 || this.course.maxTeamComplaints! > 0) && @@ -295,10 +296,13 @@ export class CourseUpdateComponent implements OnInit { if (this.communicationEnabled && this.messagingEnabled) { course['courseInformationSharingConfiguration'] = CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING; + course.faqEnabled = this.faqEnabled } else if (this.communicationEnabled && !this.messagingEnabled) { course['courseInformationSharingConfiguration'] = CourseInformationSharingConfiguration.COMMUNICATION_ONLY; + course.faqEnabled = this.faqEnabled } else { this.communicationEnabled = false; + this.faqEnabled = false course['courseInformationSharingConfiguration'] = CourseInformationSharingConfiguration.DISABLED; } @@ -650,7 +654,9 @@ export class CourseUpdateComponent implements OnInit { disableMessaging() { this.messagingEnabled = false; + this.faqEnabled = false } + } const CourseValidator: ValidatorFn = (formGroup: FormGroup) => { diff --git a/src/main/webapp/app/course/manage/overview/course-management-card.component.html b/src/main/webapp/app/course/manage/overview/course-management-card.component.html index adf01ba9af77..d13dee53b6e5 100644 --- a/src/main/webapp/app/course/manage/overview/course-management-card.component.html +++ b/src/main/webapp/app/course/manage/overview/course-management-card.component.html @@ -338,5 +338,17 @@

} + + @if (course.isAtLeastInstructor && course.faqEnabled) { + + + + + }

diff --git a/src/main/webapp/app/course/manage/overview/course-management-card.component.ts b/src/main/webapp/app/course/manage/overview/course-management-card.component.ts index 32dca0a3fc2d..e5fdaec28a2e 100644 --- a/src/main/webapp/app/course/manage/overview/course-management-card.component.ts +++ b/src/main/webapp/app/course/manage/overview/course-management-card.component.ts @@ -22,6 +22,7 @@ import { faSpinner, faTable, faUserCheck, + faQuestion } from '@fortawesome/free-solid-svg-icons'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; @@ -77,6 +78,7 @@ export class CourseManagementCardComponent implements OnChanges { faAngleUp = faAngleUp; faPersonChalkboard = faPersonChalkboard; faSpinner = faSpinner; + faQuestion = faQuestion courseColor: string; diff --git a/src/main/webapp/app/entities/course.model.ts b/src/main/webapp/app/entities/course.model.ts index 4f179de3a687..cd61cefdec33 100644 --- a/src/main/webapp/app/entities/course.model.ts +++ b/src/main/webapp/app/entities/course.model.ts @@ -62,6 +62,7 @@ export class Course implements BaseEntity { public color?: string; public courseIcon?: string; public onlineCourse?: boolean; + public faqEnabled?: boolean public enrollmentEnabled?: boolean; public enrollmentConfirmationMessage?: string; public unenrollmentEnabled?: boolean; diff --git a/src/main/webapp/app/entities/faq-category.model.ts b/src/main/webapp/app/entities/faq-category.model.ts new file mode 100644 index 000000000000..6d62502ac923 --- /dev/null +++ b/src/main/webapp/app/entities/faq-category.model.ts @@ -0,0 +1,29 @@ +export class FaqCategory { + public color?: string; + + public category?: string; + + constructor(category: string | undefined, color: string | undefined) { + this.color = color; + this.category = category; + } + + equals(otherExerciseCategory: FaqCategory): boolean { + return this.color === otherExerciseCategory.color && this.category === otherExerciseCategory.category; + } + + /** + * @param otherExerciseCategory + * @returns the alphanumerical order of the two exercise categories based on their display text + */ + compare(otherExerciseCategory: FaqCategory): number { + if (this.category === otherExerciseCategory.category) { + return 0; + } + + const displayText = this.category?.toLowerCase() ?? ''; + const otherCategoryDisplayText = otherExerciseCategory.category?.toLowerCase() ?? ''; + + return displayText < otherCategoryDisplayText ? -1 : 1; + } +} diff --git a/src/main/webapp/app/entities/faq.model.ts b/src/main/webapp/app/entities/faq.model.ts new file mode 100644 index 000000000000..ea28d55b5c5f --- /dev/null +++ b/src/main/webapp/app/entities/faq.model.ts @@ -0,0 +1,24 @@ +import { BaseEntity } from 'app/shared/model/base-entity'; +import { Course } from 'app/entities/course.model'; +import {FaqCategory} from "app/entities/faq-category.model"; + +export enum FaqState{ + ACCEPTED, REJECTED, PROPOSED +} + +export class Faq implements BaseEntity { + public id?: number; + public questionTitle?: string; + public questionAnswer?: string; + public faqState? : FaqState + public course? : Course + public categories?: FaqCategory[] + + // + isAtLeastEditor?: boolean; + isAtLeastInstructor?: boolean; + + + constructor() { + } +} diff --git a/src/main/webapp/app/faq/faq-update.component.html b/src/main/webapp/app/faq/faq-update.component.html new file mode 100644 index 000000000000..d62dd2535ff7 --- /dev/null +++ b/src/main/webapp/app/faq/faq-update.component.html @@ -0,0 +1,52 @@ +
+
+ @if (true) { +
+
+
+
+

+
+
+
+
+
+ +
+ +
+
+
+ + +
+
+ + + +
+ @if (faq.course) { +
+ +
+ +
+
+ } +
+
+ + +
+
+ +
+ +
+ } +
+
diff --git a/src/main/webapp/app/faq/faq-update.component.scss b/src/main/webapp/app/faq/faq-update.component.scss new file mode 100644 index 000000000000..0e27c3189cd2 --- /dev/null +++ b/src/main/webapp/app/faq/faq-update.component.scss @@ -0,0 +1,7 @@ +.markdown-editor { + height: 350px; +} + + + + diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts new file mode 100644 index 000000000000..a9b796614a0e --- /dev/null +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -0,0 +1,154 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { AlertService } from 'app/core/util/alert.service'; +import { CourseManagementService } from '../course/manage/course-management.service'; +import { Course } from 'app/entities/course.model'; +import { onError } from 'app/shared/util/global.utils'; +import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; +import { faBan, faQuestionCircle, faSave } from '@fortawesome/free-solid-svg-icons'; +import { MonacoFormulaAction } from 'app/shared/monaco-editor/model/actions/monaco-formula.action'; +import { Faq } from 'app/entities/faq.model'; +import { FaqService } from 'app/faq/faq.service'; + +import { FaqCategory } from 'app/entities/faq-category.model'; +import { loadCourseFaqCategories } from 'app/faq/faq.utils'; + +@Component({ + selector: 'jhi-faq-update', + templateUrl: './faq-update.component.html', + styleUrls: ['./faq-update.component.scss'], +}) +export class FAQUpdateComponent implements OnInit { + + faq: Faq; + isSaving: boolean; + existingCategories: FaqCategory[] = [] + exerciseCategories: FaqCategory[] = [] + + courses: Course[]; + + domainActionsDescription = [new MonacoFormulaAction()]; + file: File; + fileName: string; + + // Icons + faQuestionCircle = faQuestionCircle; + faSave = faSave; + faBan = faBan; + + constructor( + protected alertService: AlertService, + protected faqService : FaqService, + protected courseService: CourseManagementService, + protected activatedRoute: ActivatedRoute, + private navigationUtilService: ArtemisNavigationUtilService, + private router: Router, + ) {} + + /** + * Life cycle hook called by Angular to indicate that Angular is done creating the component + */ + ngOnInit() { + this.isSaving = false; + this.activatedRoute.parent!.data.subscribe((data) => { + // Create a new faq to use unless we fetch an existing faq + const faq = data['faq']; + this.faq = faq ?? new Faq(); + const course = data['course']; + if (course) { + this.faq.course = course; + this.loadCourseFaqCategories(course.id) + } + if(faq.categories){ + this.exerciseCategories = faq.categories + } + }); + + } + + /** + * Revert to the previous state, equivalent with pressing the back button on your browser + * Returns to the detail page if there is no previous state and we edited an existing faq + * Returns to the overview page if there is no previous state and we created a new faq + */ + + previousState() { + this.navigationUtilService.navigateBack(['course-management', this.faq.course!.id!.toString(), 'faqs']); + } + /** + * Save the changes on a faq + * This function is called by pressing save after creating or editing a faq + */ + save() { + this.isSaving = true; + if (this.faq.id !== undefined) { + this.subscribeToSaveResponse(this.faqService.update(this.faq)); + } else { + // Newly created faq must have a channel name, which cannot be undefined + console.log(this.faq) + this.subscribeToSaveResponse(this.faqService.create(this.faq)); + + } + } + + /** + * @param result The Http response from the server + */ + protected subscribeToSaveResponse(result: Observable>) { + result.subscribe({ + next: (response: HttpResponse) => this.onSaveSuccess(response.body!), + error: (error: HttpErrorResponse) => this.onSaveError(error), + }); + } + + /** + * Action on successful faq creation or edit + */ + protected onSaveSuccess(faq: Faq) { + if (!this.faq.id) { + this.faqService.find(faq.id!).subscribe({ + next: (response: HttpResponse) => { + this.isSaving = false; + this.faq = response.body!; + this.alertService.success(`FAQ with title ${faq.questionTitle} was successfully created.`); + + }, + }); + } + else { + this.isSaving = false; + this.router.navigate(['course-management', faq.course!.id, 'faqs']); + } + } + + /** + * Action on unsuccessful faq creation or edit + * @param errorRes the errorRes handed to the alert service + */ + protected onSaveError(errorRes: HttpErrorResponse) { + this.isSaving = false; + if (errorRes.error && errorRes.error.title) { + this.alertService.addErrorAlert(errorRes.error.title, errorRes.error.message, errorRes.error.params); + } else { + onError(this.alertService, errorRes); + } + } + + updateCategories(categories: FaqCategory[]) { + this.faq.categories = categories; + this.exerciseCategories = categories; + } + + private loadCourseFaqCategories(courseId: number) { + loadCourseFaqCategories(courseId, this.alertService, this.faqService).subscribe((existingCategories) => { + this.existingCategories = existingCategories; + }); + } + + canSave(){ + return this.faq.questionTitle && this.faq.questionAnswer + } + +} diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html new file mode 100644 index 000000000000..b96feba1aa5d --- /dev/null +++ b/src/main/webapp/app/faq/faq.component.html @@ -0,0 +1,123 @@ +
+
+
+

+ +

+
+
+
+
+ +
    + @for (category of existingCategories; track category){ +
  • + +
  • + } +
+
+ +
+
+
+
+ @if (true) { +
+ + + + + + + + + + + + @for (faq of filteredFaq; track trackId(i, faq); let i = $index) { + + + + + + + + + } + +
+ + + + + + + + + + + +
+ {{ faq.id }} + + {{ faq.questionTitle }} + + {{ faq.questionAnswer }} + + @for (category of faq.categories; track category) { + + } + +
+
+ @if (true) { + + + + + } + @if (true) { + + } +
+
+
+
+ } +
diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts new file mode 100644 index 000000000000..27b185bc92c3 --- /dev/null +++ b/src/main/webapp/app/faq/faq.component.ts @@ -0,0 +1,144 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Faq } from 'app/entities/faq.model'; +import { + faEdit, + faFile, + faFileExport, + faFileImport, + faFilter, + faPencilAlt, + faPlus, + faPuzzlePiece, + faSort, + faTrash +} from '@fortawesome/free-solid-svg-icons'; +import { Subject } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { AlertService } from 'app/core/util/alert.service'; +import { ActivatedRoute } from '@angular/router'; +import { FaqService } from 'app/faq/faq.service'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { onError } from 'app/shared/util/global.utils'; +import { FaqCategory } from 'app/entities/faq-category.model'; +import { loadCourseFaqCategories } from 'app/faq/faq.utils'; +import { SortService } from 'app/shared/service/sort.service'; + +@Component({ + selector: 'jhi-faq', + templateUrl: './faq.component.html' + +}) + +export class FAQComponent implements OnInit, OnDestroy { + faqs: Faq[]; + filteredFaq: Faq[]; + existingCategories: FaqCategory[] + courseId: number; + + private dialogErrorSource = new Subject(); + dialogError$ = this.dialogErrorSource.asObservable(); + + activeFilters = new Set(); + predicate: string; + ascending: boolean; + + irisEnabled = false; + + // Icons + faEdit = faEdit; + faPlus = faPlus; + faFileImport = faFileImport; + faFileExport = faFileExport; + faTrash = faTrash; + faPencilAlt = faPencilAlt; + faFile = faFile; + faPuzzlePiece = faPuzzlePiece; + faFilter = faFilter; + faSort = faSort; + + constructor( + protected faqService: FaqService, + private route: ActivatedRoute, + private alertService: AlertService, + private sortService: SortService, + ) { + this.predicate = 'id'; + this.ascending = true; + } + + ngOnInit() { + this.courseId = Number(this.route.snapshot.paramMap.get('courseId')); + this.loadAll() + this.loadCourseExerciseCategories(this.courseId) + } + + ngOnDestroy(): void { + this.dialogErrorSource.unsubscribe(); + } + + trackId(index: number, item: Faq) { + return item.id; + } + + deleteFaq(faqId: number) { + this.faqService.delete(faqId).subscribe({ + next: () => + this.handleDeleteSuccess(faqId), + error: (error: HttpErrorResponse) => this.dialogErrorSource.next(error.message), + }); + } + + private handleDeleteSuccess(faqId: number) { + this.faqs = this.faqs.filter(faq => faq.id !== faqId); + this.dialogErrorSource.next(''); + this.applyFilters(); + } + + toggleFilters(category: String) { + this.activeFilters.has(category)? this.activeFilters.delete(category) : this.activeFilters.add(category) + this.applyFilters(); + } + + sortRows() { + this.sortService.sortByProperty(this.filteredFaq, this.predicate, this.ascending); + } + + private loadAll() { + this.faqService.findAllByCourseId(this.courseId) + .pipe( + map((res: HttpResponse) => res.body), + ) + .subscribe({ + next: (res: Faq[]) => { + this.faqs = res; + this.applyFilters() + }, + error: (res: HttpErrorResponse) => onError(this.alertService, res), + }); + } + + private loadCourseExerciseCategories(courseId: number) { + loadCourseFaqCategories(courseId, this.alertService, this.faqService).subscribe((existingCategories) => { + this.existingCategories = existingCategories; + }); + } + + + private applyFilters(): void { + if (this.activeFilters.size === 0) { + // If no filters selected, show all faqs + this.filteredFaq = this.faqs; + } else { + this.filteredFaq = this.faqs.filter((faq) => this.hasFilteredCategory(faq, this.activeFilters)); + } + + } + + public hasFilteredCategory(faq: Faq, filteredCategory: Set){ + let categories = faq.categories?.map((category) => category.category) + if(categories){ + return categories.some(category => filteredCategory.has(category!)); + } + + } +} diff --git a/src/main/webapp/app/faq/faq.module.ts b/src/main/webapp/app/faq/faq.module.ts new file mode 100644 index 000000000000..09e80ef26682 --- /dev/null +++ b/src/main/webapp/app/faq/faq.module.ts @@ -0,0 +1,34 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { CompetencyFormComponent } from 'app/course/competencies/forms/competency/competency-form.component'; +import { FAQComponent } from 'app/faq/faq.component'; +import { faqRoutes } from 'app/faq/faq.routes'; +import { FAQUpdateComponent } from 'app/faq/faq-update.component'; +import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; +import { FormDateTimePickerModule } from 'app/shared/date-time-picker/date-time-picker.module'; +import { ArtemisCategorySelectorModule } from 'app/shared/category-selector/category-selector.module'; +import { + CustomExerciseCategoryBadgeComponent +} from "app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component"; +const ENTITY_STATES = [...faqRoutes]; + +@NgModule({ + imports: [ + ArtemisSharedModule, + RouterModule.forChild(ENTITY_STATES), + ArtemisSharedComponentModule, + CompetencyFormComponent, + ArtemisMarkdownEditorModule, + FormDateTimePickerModule, + ArtemisCategorySelectorModule, + CustomExerciseCategoryBadgeComponent, + + ], + declarations: [ + FAQUpdateComponent, + FAQComponent + ], +}) +export class ArtemisFAQModule {} diff --git a/src/main/webapp/app/faq/faq.routes.ts b/src/main/webapp/app/faq/faq.routes.ts new file mode 100644 index 000000000000..ed772543c6d6 --- /dev/null +++ b/src/main/webapp/app/faq/faq.routes.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Resolve, Routes } from '@angular/router'; +import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; +import { Observable, of } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { Authority } from 'app/shared/constants/authority.constants'; +import { CourseManagementResolve } from 'app/course/manage/course-management-resolve.service'; +import { CourseManagementTabBarComponent } from 'app/course/manage/course-management-tab-bar/course-management-tab-bar.component'; +import { FAQComponent } from 'app/faq/faq.component'; +import { FaqService } from 'app/faq/faq.service'; +import { Faq } from 'app/entities/faq.model'; +import { FAQUpdateComponent } from 'app/faq/faq-update.component'; + + +@Injectable({ providedIn: 'root' }) +export class FAQResolve implements Resolve { + constructor(private faqService: FaqService) {} + + resolve(route: ActivatedRouteSnapshot): Observable { + const faqId = route.params['faqId']; + if (faqId) { + return this.faqService.find(faqId).pipe( + filter((response: HttpResponse) => response.ok), + map((faq: HttpResponse) => faq.body!), + ); + } + return of(new Faq()); + } +} + + +export const faqRoutes: Routes = [ + { + path: ':courseId/faqs', + component: CourseManagementTabBarComponent, + children: [ + { + path: '', + component: FAQComponent, + resolve: { + course: CourseManagementResolve, + }, + data: { + authorities: [Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], + pageTitle: '', + }, + canActivate: [UserRouteAccessService], + }, + { + // Create a new path without a component defined to prevent the FAQ from being always rendered + path: '', + resolve: { + course: CourseManagementResolve, + }, + children: [ + { + path: 'new', + component: FAQUpdateComponent, + data: { + authorities: [Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], + pageTitle: 'global.generic.create', + + }, + canActivate: [UserRouteAccessService], + }, + { + path: ':faqId', + resolve: { + faq: FAQResolve, + }, + children: [ + { + path: 'edit', + component: FAQUpdateComponent, + data: { + authorities: [Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], + pageTitle: 'global.generic.edit', + }, + canActivate: [UserRouteAccessService], + }, + + ], + }, + ], + }, + ], + }, +]; diff --git a/src/main/webapp/app/faq/faq.service.ts b/src/main/webapp/app/faq/faq.service.ts new file mode 100644 index 000000000000..70b6bba4fc01 --- /dev/null +++ b/src/main/webapp/app/faq/faq.service.ts @@ -0,0 +1,149 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Faq, FaqState } from 'app/entities/faq.model' +import { Exercise } from 'app/entities/exercise.model'; +import { FaqCategory } from 'app/entities/faq-category.model'; +import { AlertService } from 'app/core/util/alert.service'; +import { ExerciseCategory } from 'app/entities/exercise-category.model'; + +type EntityResponseType = HttpResponse; +type EntityArrayResponseType = HttpResponse; + + +@Injectable({ providedIn: 'root' }) +export class FaqService { + + public resourceUrl = 'api/courses'; + + constructor( + protected http: HttpClient, + protected alertService: AlertService + + ) {} + + create(faq: Faq): Observable{ + let copy = FaqService.convertFaqFromClient(faq) + faq.faqState = FaqState.ACCEPTED + return this.http.post( `api/faqs`,copy, { observe: 'response' }).pipe( + map((res: EntityResponseType) => { + return res; + }), + ); + + } + + update(faq: Faq): Observable{ + let copy = FaqService.convertFaqFromClient(faq) + return this.http.put(`api/faqs/${faq.id}`, copy, { observe: 'response' }).pipe( + map((res: EntityResponseType) => { + return res; + }), + ); + + } + + find(faqId: number): Observable { + return this.http.get(`api/faqs/${faqId}`, { observe: 'response' }).pipe( + map((res: EntityResponseType) => + FaqService.convertFaqCategoriesFromServer(res) + ), + ); + } + + + findAllByCourseId(courseId: number): Observable { + return this.http + .get(this.resourceUrl+`/${courseId}/faqs`, { + observe: 'response', + }) + .pipe( + map((res: EntityArrayResponseType) => FaqService.convertExerciseCategoryArrayFromServer(res)) + ); + } + + delete(faqId: number): Observable>{ + return this.http.delete(`api/faqs/${faqId}`, { observe: 'response' }) + } + + findAllCategoriesByCourseId(courseId: number) { + return this.http.get(this.resourceUrl+`/${courseId}/faq-categories`, { + observe: 'response', + }) + } + /** + * Converts the faq category json string into FaqCategory objects (if it exists). + * @param res the response + */ + static convertFaqCategoriesFromServer(res: ERT): ERT { + if (res.body && res.body.categories) { + FaqService.parseExerciseCategories(res.body); + } + return res; + } + + /** + * Converts a faqs categories into a json string (to send them to the server). Does nothing if no categories exist + * @param faq the faq + */ + static stringifyFaqCategories(faq: Faq) { + return faq.categories?.map((category) => JSON.stringify(category) as unknown as FaqCategory); + } + + convertFaqCategoriesAsStringFromServer(categories: string[]): ExerciseCategory[] { + return categories.map((category) => JSON.parse(category)); + } + + /** + * Converts the faq category json strings into FaqCategory objects (if it exists). + * @param res the response + */ + static convertExerciseCategoryArrayFromServer(res: EART): EART { + if (res.body) { + res.body.forEach((exercise: E) => FaqService.parseExerciseCategories(exercise)); + } + return res; + } + + /** + * Parses the faq categories JSON string into {@link FaqCategory} objects. + * @param faq - the exercise + */ + static parseExerciseCategories(faq?: Faq) { + if (faq?.categories) { + faq.categories = faq.categories.map((category) => { + const categoryObj = JSON.parse(category as unknown as string); + return new FaqCategory(categoryObj.category, categoryObj.color); + }); + } + } + + static parseFaqCategoriesString(categories?: String[]) { + let faqCategories: FaqCategory[] = [] + if (categories) { + faqCategories = categories.map((category) => { + const categoryObj = JSON.parse(category as unknown as string); + return new FaqCategory(categoryObj.category, categoryObj.color); + }); + + } + return faqCategories + } + + /** + * Prepare client-faq to be uploaded to the server + * @param { Faq } faq - faq that will be modified + */ + static convertFaqFromClient(faq: F): Faq { + let copy = Object.assign(faq, {}); + copy.categories = FaqService.stringifyFaqCategories(copy); + if (copy.categories) { + + } + return copy; + } + + + +} diff --git a/src/main/webapp/app/faq/faq.utils.ts b/src/main/webapp/app/faq/faq.utils.ts new file mode 100644 index 000000000000..deb21fff6c91 --- /dev/null +++ b/src/main/webapp/app/faq/faq.utils.ts @@ -0,0 +1,33 @@ +import { onError } from 'app/shared/util/global.utils'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { AlertService } from 'app/core/util/alert.service'; +import { Observable } from 'rxjs'; +import { ExerciseCategory } from 'app/entities/exercise-category.model'; +import { FaqService } from 'app/faq/faq.service'; +import { FaqCategory } from 'app/entities/faq-category.model'; + +export function loadCourseFaqCategories( + courseId: number | undefined, + alertService: AlertService, + faqService: FaqService +): Observable { + if (courseId === undefined) { + return new Observable((observer) => { + observer.complete(); + }); + } + + return new Observable((observer) => { + faqService.findAllCategoriesByCourseId(courseId).subscribe({ + next: (categoryRes: HttpResponse) => { + const existingCategories = faqService.convertFaqCategoriesAsStringFromServer(categoryRes.body!); + observer.next(existingCategories); + observer.complete(); + }, + error: (error: HttpErrorResponse) => { + onError(alertService, error); + observer.complete(); + }, + }); + }); +} diff --git a/src/main/webapp/app/shared/category-selector/category-selector.component.ts b/src/main/webapp/app/shared/category-selector/category-selector.component.ts index d899a7b034b3..4214f340ffca 100644 --- a/src/main/webapp/app/shared/category-selector/category-selector.component.ts +++ b/src/main/webapp/app/shared/category-selector/category-selector.component.ts @@ -7,6 +7,7 @@ import { FormControl } from '@angular/forms'; import { MatChipInputEvent } from '@angular/material/chips'; import { Observable, map, startWith } from 'rxjs'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { FaqCategory } from 'app/entities/faq-category.model'; const DEFAULT_COLORS = ['#6ae8ac', '#9dca53', '#94a11c', '#691b0b', '#ad5658', '#1b97ca', '#0d3cc2', '#0ab84f']; @@ -22,12 +23,12 @@ export class CategorySelectorComponent implements OnChanges { /** * the selected categories, which can be manipulated by the user in the UI */ - @Input() categories: ExerciseCategory[]; + @Input() categories: ExerciseCategory[] | FaqCategory[]; /** * the existing categories used for auto-completion, might include duplicates */ - @Input() existingCategories: ExerciseCategory[]; + @Input() existingCategories: ExerciseCategory[] | FaqCategory[]; @Output() selectedCategories = new EventEmitter(); diff --git a/src/main/webapp/app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component.ts b/src/main/webapp/app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component.ts index 8ba41f96ba8b..aec203a26946 100644 --- a/src/main/webapp/app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component.ts +++ b/src/main/webapp/app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component.ts @@ -3,6 +3,7 @@ import type { ExerciseCategory } from 'app/entities/exercise-category.model'; import { CommonModule } from '@angular/common'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { FaqCategory } from 'app/entities/faq-category.model'; type CategoryFontSize = 'default' | 'small'; @@ -16,7 +17,7 @@ type CategoryFontSize = 'default' | 'small'; export class CustomExerciseCategoryBadgeComponent { protected readonly faTimes = faTimes; - @Input({ required: true }) category: ExerciseCategory; + @Input({ required: true }) category: ExerciseCategory | FaqCategory; @Input() displayRemoveButton: boolean = false; @Input() onClick: () => void = () => {}; @Input() fontSize: CategoryFontSize = 'default'; From 16ddd8cc810301aac125de1a10870b4691c5516d Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 10 Sep 2024 20:00:44 +0200 Subject: [PATCH 28/68] refactored toggleFilters to make commits work --- src/main/webapp/app/faq/faq.component.ts | 70 ++++++++++-------------- 1 file changed, 28 insertions(+), 42 deletions(-) diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index 27b185bc92c3..37cf46143595 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -1,17 +1,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Faq } from 'app/entities/faq.model'; -import { - faEdit, - faFile, - faFileExport, - faFileImport, - faFilter, - faPencilAlt, - faPlus, - faPuzzlePiece, - faSort, - faTrash -} from '@fortawesome/free-solid-svg-icons'; +import { faEdit, faFile, faFileExport, faFileImport, faFilter, faPencilAlt, faPlus, faPuzzlePiece, faSort, faTrash } from '@fortawesome/free-solid-svg-icons'; import { Subject } from 'rxjs'; import { map } from 'rxjs/operators'; import { AlertService } from 'app/core/util/alert.service'; @@ -25,20 +14,18 @@ import { SortService } from 'app/shared/service/sort.service'; @Component({ selector: 'jhi-faq', - templateUrl: './faq.component.html' - + templateUrl: './faq.component.html', }) - export class FAQComponent implements OnInit, OnDestroy { faqs: Faq[]; filteredFaq: Faq[]; - existingCategories: FaqCategory[] + existingCategories: FaqCategory[]; courseId: number; private dialogErrorSource = new Subject(); dialogError$ = this.dialogErrorSource.asObservable(); - activeFilters = new Set(); + activeFilters = new Set(); predicate: string; ascending: boolean; @@ -68,8 +55,8 @@ export class FAQComponent implements OnInit, OnDestroy { ngOnInit() { this.courseId = Number(this.route.snapshot.paramMap.get('courseId')); - this.loadAll() - this.loadCourseExerciseCategories(this.courseId) + this.loadAll(); + this.loadCourseExerciseCategories(this.courseId); } ngOnDestroy(): void { @@ -82,20 +69,23 @@ export class FAQComponent implements OnInit, OnDestroy { deleteFaq(faqId: number) { this.faqService.delete(faqId).subscribe({ - next: () => - this.handleDeleteSuccess(faqId), + next: () => this.handleDeleteSuccess(faqId), error: (error: HttpErrorResponse) => this.dialogErrorSource.next(error.message), }); } private handleDeleteSuccess(faqId: number) { - this.faqs = this.faqs.filter(faq => faq.id !== faqId); + this.faqs = this.faqs.filter((faq) => faq.id !== faqId); this.dialogErrorSource.next(''); this.applyFilters(); } - toggleFilters(category: String) { - this.activeFilters.has(category)? this.activeFilters.delete(category) : this.activeFilters.add(category) + toggleFilters(category: string) { + if (this.activeFilters.has(category)) { + this.activeFilters.delete(category); + } else { + this.activeFilters.add(category); + } this.applyFilters(); } @@ -104,17 +94,16 @@ export class FAQComponent implements OnInit, OnDestroy { } private loadAll() { - this.faqService.findAllByCourseId(this.courseId) - .pipe( - map((res: HttpResponse) => res.body), - ) - .subscribe({ - next: (res: Faq[]) => { - this.faqs = res; - this.applyFilters() - }, - error: (res: HttpErrorResponse) => onError(this.alertService, res), - }); + this.faqService + .findAllByCourseId(this.courseId) + .pipe(map((res: HttpResponse) => res.body)) + .subscribe({ + next: (res: Faq[]) => { + this.faqs = res; + this.applyFilters(); + }, + error: (res: HttpErrorResponse) => onError(this.alertService, res), + }); } private loadCourseExerciseCategories(courseId: number) { @@ -123,7 +112,6 @@ export class FAQComponent implements OnInit, OnDestroy { }); } - private applyFilters(): void { if (this.activeFilters.size === 0) { // If no filters selected, show all faqs @@ -131,14 +119,12 @@ export class FAQComponent implements OnInit, OnDestroy { } else { this.filteredFaq = this.faqs.filter((faq) => this.hasFilteredCategory(faq, this.activeFilters)); } - } - public hasFilteredCategory(faq: Faq, filteredCategory: Set){ - let categories = faq.categories?.map((category) => category.category) - if(categories){ - return categories.some(category => filteredCategory.has(category!)); + public hasFilteredCategory(faq: Faq, filteredCategory: Set) { + const categories = faq.categories?.map((category) => category.category); + if (categories) { + return categories.some((category) => filteredCategory.has(category!)); } - } } From 019349d6e3012de63b109a00bd8f8d14d2ec45c4 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 11 Sep 2024 16:17:33 +0200 Subject: [PATCH 29/68] Added integration test, but they do not work yet --- .../tum/in/www1/artemis/faq/FaqFactory.java | 29 ++++ .../www1/artemis/faq/FaqIntegrationTest.java | 124 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java create mode 100644 src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java diff --git a/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java b/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java new file mode 100644 index 000000000000..64e08ae1742b --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java @@ -0,0 +1,29 @@ +package de.tum.in.www1.artemis.faq; + +import java.util.HashSet; +import java.util.Set; + +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.Faq; +import de.tum.in.www1.artemis.domain.FaqState; + +public class FaqFactory { + + public static Faq generateFaq(Long id, Course course) { + Faq faq = new Faq(); + faq.setId(id); + faq.setCourse(course); + faq.setFaqState(FaqState.ACCEPTED); + faq.setQuestionAnswer("Answer"); + faq.setQuestionTitle("Title"); + faq.setCategories(generateFaqCategories()); + return faq; + } + + public static Set generateFaqCategories() { + HashSet categories = new HashSet<>(); + categories.add("this is a category"); + categories.add("this is also a category"); + return categories; + } +} diff --git a/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java new file mode 100644 index 000000000000..89c9c8f5b11b --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java @@ -0,0 +1,124 @@ +package de.tum.in.www1.artemis.faq; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; + +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.Faq; +import de.tum.in.www1.artemis.domain.FaqState; +import de.tum.in.www1.artemis.repository.FaqRepository; + +class FaqIntegrationTest extends AbstractSpringIntegrationIndependentTest { + + private static final String TEST_PREFIX = "faqIntegrationTest"; + + @Autowired + private FaqRepository faqRepository; + + private Course course1; + + private Faq faq; + + @BeforeEach + void initTestCase() { + int numberOfTutors = 2; + long courseId = 2; + long faqId = 1; + userUtilService.addUsers(TEST_PREFIX, 1, numberOfTutors, 0, 1); + this.course1 = courseUtilService.createCourse(courseId); + this.faq = FaqFactory.generateFaq(faqId, course1); + faqRepository.save(this.faq); + this.course1.addFaq(this.faq); + // Add users that are not in the course + userUtilService.createAndSaveUser(TEST_PREFIX + "student42"); + userUtilService.createAndSaveUser(TEST_PREFIX + "instructor42"); + } + + private void testAllPreAuthorize() throws Exception { + request.postWithResponseBody("/api/faqs", new Faq(), Faq.class, HttpStatus.FORBIDDEN); + System.out.println("Test"); + request.putWithResponseBody("/api/faqs/" + faq.getId(), new Faq(), Faq.class, HttpStatus.FORBIDDEN); + request.getList("/api/courses/" + course1.getId() + "/faqs", HttpStatus.FORBIDDEN, Faq.class); + request.delete("/api/faqs/" + faq.getId(), HttpStatus.FORBIDDEN); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") + void testAll_asTutor() throws Exception { + this.testAllPreAuthorize(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testAll_asStudent() throws Exception { + this.testAllPreAuthorize(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void createFaq_correctRequestBody_shouldCreateFaq() throws Exception { + Course course = courseRepository.findByIdElseThrow(this.course1.getId()); + + Faq faq = new Faq(); + faq.setQuestionTitle("Title"); + faq.setQuestionAnswer("Answer"); + faq.setCategories(FaqFactory.generateFaqCategories()); + faq.setFaqState(FaqState.ACCEPTED); + faq.setCourse(course); + + Faq returnedFaq = request.postWithResponseBody("/api/faqs", faq, Faq.class, HttpStatus.CREATED); + + assertThat(returnedFaq).isNotNull(); + assertThat(returnedFaq.getId()).isNotNull(); + assertThat(returnedFaq.getQuestionTitle()).isEqualTo(faq.getQuestionTitle()); + assertThat(returnedFaq.getCourse().getId()).isEqualTo(faq.getCourse().getId()); + assertThat(returnedFaq.getQuestionAnswer()).isEqualTo(faq.getQuestionAnswer()); + assertThat(returnedFaq.getCategories()).isEqualTo(faq.getCategories()); + assertThat(returnedFaq.getFaqState()).isEqualTo(faq.getFaqState()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void createFaq_alreadyId_shouldReturnBadRequest() throws Exception { + Faq faq = new Faq(); + faq.setId(1L); + request.postWithResponseBody("/api/faqs", faq, Faq.class, HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void updateFaq_correctRequestBody_shouldUpdateFaq() throws Exception { + Faq faq = faqRepository.findById(this.faq.getId()).orElseThrow(); + faq.setQuestionTitle("Updated"); + faq.setQuestionAnswer("Updated"); + faq.setFaqState(FaqState.PROPOSED); + Set newCategories = new HashSet(); + newCategories.add("Test"); + faq.setCategories(newCategories); + Faq updatedFaq = request.putWithResponseBody("/api/faqs/" + faq.getId(), faq, Faq.class, HttpStatus.OK); + + assertThat(updatedFaq.getQuestionTitle()).isEqualTo("Updated"); + assertThat(updatedFaq.getQuestionTitle()).isEqualTo("Updated"); + assertThat(updatedFaq.getFaqState()).isEqualTo(FaqState.REJECTED); + assertThat(updatedFaq.getCategories()).isEqualTo(newCategories); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetFaqCategoriesByCourseId() throws Exception { + Faq faq = faqRepository.findById(this.faq.getId()).orElseThrow(); + Set categories = faq.getCategories(); + Set returnedCategories = request.get("/api/courses/" + faq.getCourse().getId() + "/faq-categories", HttpStatus.OK, Set.class); + assertThat(categories).isEqualTo(returnedCategories); + } + +} From 4ab0edfa5091c3884d8f704fd827eb8d9af82904 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 11 Sep 2024 16:32:11 +0200 Subject: [PATCH 30/68] Integration Tests --- .../de/tum/cit/aet/artemis/modeling/service/FaqService.java | 2 +- .../java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/modeling/service/FaqService.java b/src/main/java/de/tum/cit/aet/artemis/modeling/service/FaqService.java index 0f213a1fc8e2..0a47ed169292 100644 --- a/src/main/java/de/tum/cit/aet/artemis/modeling/service/FaqService.java +++ b/src/main/java/de/tum/cit/aet/artemis/modeling/service/FaqService.java @@ -18,7 +18,7 @@ public FaqService(FaqRepository faqRepository) { } /** - * Deletes the given lecture (with its lecture units). + * Deletes the given faq * * @param faqId the faqId of to be deleted faq */ diff --git a/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java index 89c9c8f5b11b..720a658daced 100644 --- a/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java @@ -38,9 +38,6 @@ void initTestCase() { this.faq = FaqFactory.generateFaq(faqId, course1); faqRepository.save(this.faq); this.course1.addFaq(this.faq); - // Add users that are not in the course - userUtilService.createAndSaveUser(TEST_PREFIX + "student42"); - userUtilService.createAndSaveUser(TEST_PREFIX + "instructor42"); } private void testAllPreAuthorize() throws Exception { From b433a3a4cbbd99afa45ba580a8616fc0d9072815 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 12 Sep 2024 12:43:57 +0200 Subject: [PATCH 31/68] Integration Tests fixed. Why so ever its works now --- .../tum/in/www1/artemis/faq/FaqFactory.java | 3 +- .../www1/artemis/faq/FaqIntegrationTest.java | 49 ++++++++----------- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java b/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java index 64e08ae1742b..815940598395 100644 --- a/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java +++ b/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java @@ -9,9 +9,8 @@ public class FaqFactory { - public static Faq generateFaq(Long id, Course course) { + public static Faq generateFaq(Course course) { Faq faq = new Faq(); - faq.setId(id); faq.setCourse(course); faq.setFaqState(FaqState.ACCEPTED); faq.setQuestionAnswer("Answer"); diff --git a/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java index 720a658daced..b3f62be75e70 100644 --- a/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.util.HashSet; +import java.util.List; import java.util.Set; import org.junit.jupiter.api.BeforeEach; @@ -19,7 +20,7 @@ class FaqIntegrationTest extends AbstractSpringIntegrationIndependentTest { - private static final String TEST_PREFIX = "faqIntegrationTest"; + private static final String TEST_PREFIX = "faqintegrationtest"; @Autowired private FaqRepository faqRepository; @@ -29,23 +30,24 @@ class FaqIntegrationTest extends AbstractSpringIntegrationIndependentTest { private Faq faq; @BeforeEach - void initTestCase() { + void initTestCase() throws Exception { int numberOfTutors = 2; - long courseId = 2; - long faqId = 1; userUtilService.addUsers(TEST_PREFIX, 1, numberOfTutors, 0, 1); - this.course1 = courseUtilService.createCourse(courseId); - this.faq = FaqFactory.generateFaq(faqId, course1); + List courses = courseUtilService.createCoursesWithExercisesAndLectures(TEST_PREFIX, true, true, numberOfTutors); + this.course1 = this.courseRepository.findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(courses.getFirst().getId()); + this.faq = FaqFactory.generateFaq(course1); faqRepository.save(this.faq); - this.course1.addFaq(this.faq); + // Add users that are not in the course + userUtilService.createAndSaveUser(TEST_PREFIX + "student42"); + userUtilService.createAndSaveUser(TEST_PREFIX + "instructor42"); + } private void testAllPreAuthorize() throws Exception { request.postWithResponseBody("/api/faqs", new Faq(), Faq.class, HttpStatus.FORBIDDEN); - System.out.println("Test"); - request.putWithResponseBody("/api/faqs/" + faq.getId(), new Faq(), Faq.class, HttpStatus.FORBIDDEN); + request.putWithResponseBody("/api/faqs/" + this.faq.getId(), this.faq, Faq.class, HttpStatus.FORBIDDEN); request.getList("/api/courses/" + course1.getId() + "/faqs", HttpStatus.FORBIDDEN, Faq.class); - request.delete("/api/faqs/" + faq.getId(), HttpStatus.FORBIDDEN); + request.delete("/api/faqs/" + this.faq.getId(), HttpStatus.FORBIDDEN); } @Test @@ -63,24 +65,15 @@ void testAll_asStudent() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createFaq_correctRequestBody_shouldCreateFaq() throws Exception { - Course course = courseRepository.findByIdElseThrow(this.course1.getId()); - - Faq faq = new Faq(); - faq.setQuestionTitle("Title"); - faq.setQuestionAnswer("Answer"); - faq.setCategories(FaqFactory.generateFaqCategories()); - faq.setFaqState(FaqState.ACCEPTED); - faq.setCourse(course); - - Faq returnedFaq = request.postWithResponseBody("/api/faqs", faq, Faq.class, HttpStatus.CREATED); - + Faq newFaq = FaqFactory.generateFaq(course1); + Faq returnedFaq = request.postWithResponseBody("/api/faqs", newFaq, Faq.class, HttpStatus.CREATED); assertThat(returnedFaq).isNotNull(); assertThat(returnedFaq.getId()).isNotNull(); - assertThat(returnedFaq.getQuestionTitle()).isEqualTo(faq.getQuestionTitle()); - assertThat(returnedFaq.getCourse().getId()).isEqualTo(faq.getCourse().getId()); - assertThat(returnedFaq.getQuestionAnswer()).isEqualTo(faq.getQuestionAnswer()); - assertThat(returnedFaq.getCategories()).isEqualTo(faq.getCategories()); - assertThat(returnedFaq.getFaqState()).isEqualTo(faq.getFaqState()); + assertThat(returnedFaq.getQuestionTitle()).isEqualTo(newFaq.getQuestionTitle()); + assertThat(returnedFaq.getCourse().getId()).isEqualTo(newFaq.getCourse().getId()); + assertThat(returnedFaq.getQuestionAnswer()).isEqualTo(newFaq.getQuestionAnswer()); + assertThat(returnedFaq.getCategories()).isEqualTo(newFaq.getCategories()); + assertThat(returnedFaq.getFaqState()).isEqualTo(newFaq.getFaqState()); } @Test @@ -98,14 +91,14 @@ void updateFaq_correctRequestBody_shouldUpdateFaq() throws Exception { faq.setQuestionTitle("Updated"); faq.setQuestionAnswer("Updated"); faq.setFaqState(FaqState.PROPOSED); - Set newCategories = new HashSet(); + Set newCategories = new HashSet<>(); newCategories.add("Test"); faq.setCategories(newCategories); Faq updatedFaq = request.putWithResponseBody("/api/faqs/" + faq.getId(), faq, Faq.class, HttpStatus.OK); assertThat(updatedFaq.getQuestionTitle()).isEqualTo("Updated"); assertThat(updatedFaq.getQuestionTitle()).isEqualTo("Updated"); - assertThat(updatedFaq.getFaqState()).isEqualTo(FaqState.REJECTED); + assertThat(updatedFaq.getFaqState()).isEqualTo(FaqState.PROPOSED); assertThat(updatedFaq.getCategories()).isEqualTo(newCategories); } From a232c2598add872081b9555ba9d3831fe8bca29a Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 12 Sep 2024 12:48:18 +0200 Subject: [PATCH 32/68] Formula Action change from Patrick --- .../manage/course-update.component.html | 29 ++++++------------ .../course/manage/course-update.component.ts | 11 +++---- .../webapp/app/faq/faq-update.component.ts | 30 ++++++++----------- 3 files changed, 25 insertions(+), 45 deletions(-) diff --git a/src/main/webapp/app/course/manage/course-update.component.html b/src/main/webapp/app/course/manage/course-update.component.html index 4906832adf6c..5af60571f1fd 100644 --- a/src/main/webapp/app/course/manage/course-update.component.html +++ b/src/main/webapp/app/course/manage/course-update.component.html @@ -347,26 +347,6 @@
ngbTooltip="{{ 'artemisApp.course.courseCommunicationSetting.messagingEnabled.tooltip' | artemisTranslate }}" />
-
- - - -
@if (communicationEnabled) {
@@ -393,6 +373,15 @@
+
+ + + +
@if (this.isAdmin) {
diff --git a/src/main/webapp/app/course/manage/course-update.component.ts b/src/main/webapp/app/course/manage/course-update.component.ts index 69549695b862..8d928236c268 100644 --- a/src/main/webapp/app/course/manage/course-update.component.ts +++ b/src/main/webapp/app/course/manage/course-update.component.ts @@ -71,7 +71,7 @@ export class CourseUpdateComponent implements OnInit { faExclamationTriangle = faExclamationTriangle; faPen = faPen; - faqEnabled = true + faqEnabled = true; communicationEnabled = true; messagingEnabled = true; ltiEnabled = false; @@ -116,7 +116,7 @@ export class CourseUpdateComponent implements OnInit { this.courseOrganizations = organizations; }); this.originalTimeZone = this.course.timeZone; - this.faqEnabled = course.faqEnabled + this.faqEnabled = course.faqEnabled; // complaints are only enabled when at least one complaint is allowed and the complaint duration is positive this.complaintsEnabled = (this.course.maxComplaints! > 0 || this.course.maxTeamComplaints! > 0) && @@ -296,13 +296,12 @@ export class CourseUpdateComponent implements OnInit { if (this.communicationEnabled && this.messagingEnabled) { course['courseInformationSharingConfiguration'] = CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING; - course.faqEnabled = this.faqEnabled + course.faqEnabled = this.faqEnabled; } else if (this.communicationEnabled && !this.messagingEnabled) { course['courseInformationSharingConfiguration'] = CourseInformationSharingConfiguration.COMMUNICATION_ONLY; - course.faqEnabled = this.faqEnabled + course.faqEnabled = this.faqEnabled; } else { this.communicationEnabled = false; - this.faqEnabled = false course['courseInformationSharingConfiguration'] = CourseInformationSharingConfiguration.DISABLED; } @@ -654,9 +653,7 @@ export class CourseUpdateComponent implements OnInit { disableMessaging() { this.messagingEnabled = false; - this.faqEnabled = false } - } const CourseValidator: ValidatorFn = (formGroup: FormGroup) => { diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index a9b796614a0e..97c782220858 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -8,7 +8,7 @@ import { Course } from 'app/entities/course.model'; import { onError } from 'app/shared/util/global.utils'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { faBan, faQuestionCircle, faSave } from '@fortawesome/free-solid-svg-icons'; -import { MonacoFormulaAction } from 'app/shared/monaco-editor/model/actions/monaco-formula.action'; +import { FormulaAction } from 'app/shared/monaco-editor/model/actions/formula.action'; import { Faq } from 'app/entities/faq.model'; import { FaqService } from 'app/faq/faq.service'; @@ -21,15 +21,14 @@ import { loadCourseFaqCategories } from 'app/faq/faq.utils'; styleUrls: ['./faq-update.component.scss'], }) export class FAQUpdateComponent implements OnInit { - faq: Faq; isSaving: boolean; - existingCategories: FaqCategory[] = [] - exerciseCategories: FaqCategory[] = [] + existingCategories: FaqCategory[] = []; + exerciseCategories: FaqCategory[] = []; courses: Course[]; - domainActionsDescription = [new MonacoFormulaAction()]; + domainActionsDescription = [new FormulaAction()]; file: File; fileName: string; @@ -40,7 +39,7 @@ export class FAQUpdateComponent implements OnInit { constructor( protected alertService: AlertService, - protected faqService : FaqService, + protected faqService: FaqService, protected courseService: CourseManagementService, protected activatedRoute: ActivatedRoute, private navigationUtilService: ArtemisNavigationUtilService, @@ -59,13 +58,12 @@ export class FAQUpdateComponent implements OnInit { const course = data['course']; if (course) { this.faq.course = course; - this.loadCourseFaqCategories(course.id) + this.loadCourseFaqCategories(course.id); } - if(faq.categories){ - this.exerciseCategories = faq.categories + if (faq.categories) { + this.exerciseCategories = faq.categories; } }); - } /** @@ -87,9 +85,8 @@ export class FAQUpdateComponent implements OnInit { this.subscribeToSaveResponse(this.faqService.update(this.faq)); } else { // Newly created faq must have a channel name, which cannot be undefined - console.log(this.faq) + console.log(this.faq); this.subscribeToSaveResponse(this.faqService.create(this.faq)); - } } @@ -113,11 +110,9 @@ export class FAQUpdateComponent implements OnInit { this.isSaving = false; this.faq = response.body!; this.alertService.success(`FAQ with title ${faq.questionTitle} was successfully created.`); - }, }); - } - else { + } else { this.isSaving = false; this.router.navigate(['course-management', faq.course!.id, 'faqs']); } @@ -147,8 +142,7 @@ export class FAQUpdateComponent implements OnInit { }); } - canSave(){ - return this.faq.questionTitle && this.faq.questionAnswer + canSave() { + return this.faq.questionTitle && this.faq.questionAnswer; } - } From 80cc42af49e4a78119dea43ee27ee3040fa372ce Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 12 Sep 2024 14:02:06 +0200 Subject: [PATCH 33/68] Make components standalone --- src/main/webapp/app/faq/faq-update.component.ts | 6 ++++++ src/main/webapp/app/faq/faq.component.ts | 5 +++++ src/main/webapp/app/faq/faq.module.ts | 14 +++----------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index 97c782220858..5cd81aa80fd7 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -14,11 +14,17 @@ import { FaqService } from 'app/faq/faq.service'; import { FaqCategory } from 'app/entities/faq-category.model'; import { loadCourseFaqCategories } from 'app/faq/faq.utils'; +import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; +import { ArtemisCategorySelectorModule } from 'app/shared/category-selector/category-selector.module'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; @Component({ selector: 'jhi-faq-update', templateUrl: './faq-update.component.html', styleUrls: ['./faq-update.component.scss'], + standalone: true, + imports: [ArtemisSharedModule, ArtemisSharedComponentModule, ArtemisMarkdownEditorModule, ArtemisCategorySelectorModule], }) export class FAQUpdateComponent implements OnInit { faq: Faq; diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index 37cf46143595..b942c250dc90 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -11,10 +11,15 @@ import { onError } from 'app/shared/util/global.utils'; import { FaqCategory } from 'app/entities/faq-category.model'; import { loadCourseFaqCategories } from 'app/faq/faq.utils'; import { SortService } from 'app/shared/service/sort.service'; +import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; @Component({ selector: 'jhi-faq', templateUrl: './faq.component.html', + standalone: true, + imports: [ArtemisSharedModule, CustomExerciseCategoryBadgeComponent, ArtemisSharedComponentModule], }) export class FAQComponent implements OnInit, OnDestroy { faqs: Faq[]; diff --git a/src/main/webapp/app/faq/faq.module.ts b/src/main/webapp/app/faq/faq.module.ts index 09e80ef26682..bb13f966e3b5 100644 --- a/src/main/webapp/app/faq/faq.module.ts +++ b/src/main/webapp/app/faq/faq.module.ts @@ -2,16 +2,12 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; -import { CompetencyFormComponent } from 'app/course/competencies/forms/competency/competency-form.component'; import { FAQComponent } from 'app/faq/faq.component'; import { faqRoutes } from 'app/faq/faq.routes'; import { FAQUpdateComponent } from 'app/faq/faq-update.component'; import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; -import { FormDateTimePickerModule } from 'app/shared/date-time-picker/date-time-picker.module'; import { ArtemisCategorySelectorModule } from 'app/shared/category-selector/category-selector.module'; -import { - CustomExerciseCategoryBadgeComponent -} from "app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component"; +import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; const ENTITY_STATES = [...faqRoutes]; @NgModule({ @@ -19,16 +15,12 @@ const ENTITY_STATES = [...faqRoutes]; ArtemisSharedModule, RouterModule.forChild(ENTITY_STATES), ArtemisSharedComponentModule, - CompetencyFormComponent, ArtemisMarkdownEditorModule, - FormDateTimePickerModule, ArtemisCategorySelectorModule, CustomExerciseCategoryBadgeComponent, - - ], - declarations: [ + FAQComponent, FAQUpdateComponent, - FAQComponent ], + exports: [FAQComponent, FAQUpdateComponent], }) export class ArtemisFAQModule {} From 8f71b0acf5945d7fb9fcde040de3c3d9274bad9b Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 12 Sep 2024 14:33:51 +0200 Subject: [PATCH 34/68] Removed unnecessary import statements --- src/main/webapp/app/faq/faq.module.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/main/webapp/app/faq/faq.module.ts b/src/main/webapp/app/faq/faq.module.ts index bb13f966e3b5..56765678ac1e 100644 --- a/src/main/webapp/app/faq/faq.module.ts +++ b/src/main/webapp/app/faq/faq.module.ts @@ -1,26 +1,12 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { FAQComponent } from 'app/faq/faq.component'; import { faqRoutes } from 'app/faq/faq.routes'; import { FAQUpdateComponent } from 'app/faq/faq-update.component'; -import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; -import { ArtemisCategorySelectorModule } from 'app/shared/category-selector/category-selector.module'; -import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; const ENTITY_STATES = [...faqRoutes]; @NgModule({ - imports: [ - ArtemisSharedModule, - RouterModule.forChild(ENTITY_STATES), - ArtemisSharedComponentModule, - ArtemisMarkdownEditorModule, - ArtemisCategorySelectorModule, - CustomExerciseCategoryBadgeComponent, - FAQComponent, - FAQUpdateComponent, - ], + imports: [RouterModule.forChild(ENTITY_STATES), FAQComponent, FAQUpdateComponent], exports: [FAQComponent, FAQUpdateComponent], }) export class ArtemisFAQModule {} From 7ffc7bfac4b2b189facae6e9b932c6737e6904ff Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Sat, 14 Sep 2024 16:12:40 +0200 Subject: [PATCH 35/68] Made filter to use badges, not plain text --- src/main/webapp/app/faq/faq.component.html | 68 +++++++++++----------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index b96feba1aa5d..578240a6ccf2 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -18,21 +18,19 @@

    - @for (category of existingCategories; track category){ -
  • - -
  • + @for (category of existingCategories; track category) { +
  • + +
  • }

@@ -50,31 +48,31 @@

- - - - - - - + + + + + + + @for (faq of filteredFaq; track trackId(i, faq); let i = $index) { - + @for (faq of filteredFaq; track trackId(i, faq); let i = $index) { - @for (faq of filteredFaq; track trackId(i, faq); let i = $index) { + @for (faq of filteredFaqs; track trackId(i, faq); let i = $index) {
- - - - - - - - - - - -
+ + + + + + + + + + + +
- {{ faq.id }} + {{ faq.id }} {{ faq.questionTitle }} From f68d62147424467859f1ea09451b70fcebcc9625 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Sun, 15 Sep 2024 17:09:13 +0200 Subject: [PATCH 36/68] Added Student view and filtering as a service --- src/main/webapp/app/faq/faq.component.ts | 26 +--- src/main/webapp/app/faq/faq.service.ts | 77 +++++++----- .../course-faq-accordion-component.html | 13 ++ .../course-faq-accordion-component.scss | 18 +++ .../course-faq-accordion-component.ts | 23 ++++ .../course-faq/course-faq.component.html | 43 +++++++ .../course-faq/course-faq.component.scss | 7 ++ .../course-faq/course-faq.component.ts | 114 ++++++++++++++++++ .../app/overview/course-overview.component.ts | 24 ++++ .../app/overview/courses-routing.module.ts | 11 ++ .../webapp/i18n/en/student-dashboard.json | 3 +- 11 files changed, 304 insertions(+), 55 deletions(-) create mode 100644 src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html create mode 100644 src/main/webapp/app/overview/course-faq/course-faq-accordion-component.scss create mode 100644 src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts create mode 100644 src/main/webapp/app/overview/course-faq/course-faq.component.html create mode 100644 src/main/webapp/app/overview/course-faq/course-faq.component.scss create mode 100644 src/main/webapp/app/overview/course-faq/course-faq.component.ts diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index b942c250dc90..8d918768263d 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -86,14 +86,14 @@ export class FAQComponent implements OnInit, OnDestroy { } toggleFilters(category: string) { - if (this.activeFilters.has(category)) { - this.activeFilters.delete(category); - } else { - this.activeFilters.add(category); - } + this.activeFilters = FaqService.toggleFilter(category, this.activeFilters); this.applyFilters(); } + private applyFilters(): void { + this.filteredFaq = FaqService.applyFilters(this.activeFilters, this.faqs); + } + sortRows() { this.sortService.sortByProperty(this.filteredFaq, this.predicate, this.ascending); } @@ -116,20 +116,4 @@ export class FAQComponent implements OnInit, OnDestroy { this.existingCategories = existingCategories; }); } - - private applyFilters(): void { - if (this.activeFilters.size === 0) { - // If no filters selected, show all faqs - this.filteredFaq = this.faqs; - } else { - this.filteredFaq = this.faqs.filter((faq) => this.hasFilteredCategory(faq, this.activeFilters)); - } - } - - public hasFilteredCategory(faq: Faq, filteredCategory: Set) { - const categories = faq.categories?.map((category) => category.category); - if (categories) { - return categories.some((category) => filteredCategory.has(category!)); - } - } } diff --git a/src/main/webapp/app/faq/faq.service.ts b/src/main/webapp/app/faq/faq.service.ts index 70b6bba4fc01..5a05648723d0 100644 --- a/src/main/webapp/app/faq/faq.service.ts +++ b/src/main/webapp/app/faq/faq.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { Faq, FaqState } from 'app/entities/faq.model' +import { Faq, FaqState } from 'app/entities/faq.model'; import { Exercise } from 'app/entities/exercise.model'; import { FaqCategory } from 'app/entities/faq-category.model'; import { AlertService } from 'app/core/util/alert.service'; @@ -11,66 +11,54 @@ import { ExerciseCategory } from 'app/entities/exercise-category.model'; type EntityResponseType = HttpResponse; type EntityArrayResponseType = HttpResponse; - @Injectable({ providedIn: 'root' }) export class FaqService { - public resourceUrl = 'api/courses'; constructor( protected http: HttpClient, - protected alertService: AlertService - + protected alertService: AlertService, ) {} - create(faq: Faq): Observable{ - let copy = FaqService.convertFaqFromClient(faq) - faq.faqState = FaqState.ACCEPTED - return this.http.post( `api/faqs`,copy, { observe: 'response' }).pipe( + create(faq: Faq): Observable { + const copy = FaqService.convertFaqFromClient(faq); + faq.faqState = FaqState.ACCEPTED; + return this.http.post(`api/faqs`, copy, { observe: 'response' }).pipe( map((res: EntityResponseType) => { return res; }), ); - } - update(faq: Faq): Observable{ - let copy = FaqService.convertFaqFromClient(faq) + update(faq: Faq): Observable { + const copy = FaqService.convertFaqFromClient(faq); return this.http.put(`api/faqs/${faq.id}`, copy, { observe: 'response' }).pipe( map((res: EntityResponseType) => { return res; }), ); - } find(faqId: number): Observable { - return this.http.get(`api/faqs/${faqId}`, { observe: 'response' }).pipe( - map((res: EntityResponseType) => - FaqService.convertFaqCategoriesFromServer(res) - ), - ); + return this.http.get(`api/faqs/${faqId}`, { observe: 'response' }).pipe(map((res: EntityResponseType) => FaqService.convertFaqCategoriesFromServer(res))); } - findAllByCourseId(courseId: number): Observable { return this.http - .get(this.resourceUrl+`/${courseId}/faqs`, { + .get(this.resourceUrl + `/${courseId}/faqs`, { observe: 'response', }) - .pipe( - map((res: EntityArrayResponseType) => FaqService.convertExerciseCategoryArrayFromServer(res)) - ); + .pipe(map((res: EntityArrayResponseType) => FaqService.convertExerciseCategoryArrayFromServer(res))); } - delete(faqId: number): Observable>{ - return this.http.delete(`api/faqs/${faqId}`, { observe: 'response' }) + delete(faqId: number): Observable> { + return this.http.delete(`api/faqs/${faqId}`, { observe: 'response' }); } findAllCategoriesByCourseId(courseId: number) { - return this.http.get(this.resourceUrl+`/${courseId}/faq-categories`, { + return this.http.get(this.resourceUrl + `/${courseId}/faq-categories`, { observe: 'response', - }) + }); } /** * Converts the faq category json string into FaqCategory objects (if it exists). @@ -119,16 +107,15 @@ export class FaqService { } } - static parseFaqCategoriesString(categories?: String[]) { - let faqCategories: FaqCategory[] = [] + static parseFaqCategoriesString(categories?: string[]) { + let faqCategories: FaqCategory[] = []; if (categories) { faqCategories = categories.map((category) => { const categoryObj = JSON.parse(category as unknown as string); return new FaqCategory(categoryObj.category, categoryObj.color); }); - } - return faqCategories + return faqCategories; } /** @@ -136,14 +123,38 @@ export class FaqService { * @param { Faq } faq - faq that will be modified */ static convertFaqFromClient(faq: F): Faq { - let copy = Object.assign(faq, {}); + const copy = Object.assign(faq, {}); copy.categories = FaqService.stringifyFaqCategories(copy); if (copy.categories) { - } return copy; } + static toggleFilter(category: string, activeFilters: Set) { + if (activeFilters.has(category)) { + activeFilters.delete(category); + return activeFilters; + } else { + activeFilters.add(category); + return activeFilters; + } + } + static applyFilters(activeFilters: Set, faqs: Faq[]): Faq[] { + let filteredFaq: Faq[]; + if (activeFilters.size === 0) { + // If no filters selected, show all faqs + filteredFaq = faqs; + } else { + filteredFaq = faqs.filter((faq) => this.hasFilteredCategory(faq, activeFilters)); + } + return filteredFaq; + } + public static hasFilteredCategory(faq: Faq, filteredCategory: Set) { + const categories = faq.categories?.map((category) => category.category); + if (categories) { + return categories.some((category) => filteredCategory.has(category!)); + } + } } diff --git a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html new file mode 100644 index 000000000000..965712b9ec36 --- /dev/null +++ b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html @@ -0,0 +1,13 @@ +
+
+

{{faq().questionTitle}}

+
+ @for (category of faq().categories; track category){ + + } +
+
+
+

{{faq().questionAnswer}}

+
+
diff --git a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.scss b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.scss new file mode 100644 index 000000000000..fe52693ae074 --- /dev/null +++ b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.scss @@ -0,0 +1,18 @@ +.faq-container { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 10px; + box-sizing: border-box; +} + +.faq-container h1 { + margin: 0; +} + +.badge-container { + display: flex; + margin-left: auto; + gap: 4px; +} diff --git a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts new file mode 100644 index 000000000000..b3459c8aa980 --- /dev/null +++ b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts @@ -0,0 +1,23 @@ +import { Component, OnDestroy, input } from '@angular/core'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; +import { Faq } from 'app/entities/faq.model'; +import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; +import { Subject } from 'rxjs/internal/Subject'; + +@Component({ + selector: 'jhi-course-faq-accordion', + templateUrl: './course-faq-accordion-component.html', + styleUrl: './course-faq-accordion-component.scss', + standalone: true, + + imports: [TranslateDirective, CustomExerciseCategoryBadgeComponent], +}) +export class CourseFaqAccordionComponent implements OnDestroy { + private ngUnsubscribe = new Subject(); + faq = input.required(); + + ngOnDestroy(): void { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } +} diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.html b/src/main/webapp/app/overview/course-faq/course-faq.component.html new file mode 100644 index 000000000000..b165b4145339 --- /dev/null +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.html @@ -0,0 +1,43 @@ +
+
+ + + + +
+ +
    + @for (category of existingCategories; track category) { +
  • + +
  • + } +
+
+
+ @for (faq of this.filteredFaq; track faq) { +
+ +
+ } +
diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.scss b/src/main/webapp/app/overview/course-faq/course-faq.component.scss new file mode 100644 index 000000000000..9e1c700ded25 --- /dev/null +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.scss @@ -0,0 +1,7 @@ +.second-layer-modal-bg { + background-color: var(--secondary); +} + +.module-bg { + background-color: var(--module-bg); +} diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts new file mode 100644 index 000000000000..964ba0668b41 --- /dev/null +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -0,0 +1,114 @@ +import { Component, ElementRef, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { map } from 'rxjs/operators'; +import { Subject, Subscription } from 'rxjs'; +import { MetisService } from 'app/shared/metis/metis.service'; +import { faFilter, faPlus, faSearch, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { ButtonType } from 'app/shared/components/button.component'; +import { CourseWideSearchComponent, CourseWideSearchConfig } from 'app/overview/course-conversations/course-wide-search/course-wide-search.component'; +import { SidebarData } from 'app/types/sidebar'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { CourseFaqAccordionComponent } from 'app/overview/course-faq/course-faq-accordion-component'; +import { Faq } from 'app/entities/faq.model'; +import { FaqService } from 'app/faq/faq.service'; +import { HttpResponse } from '@angular/common/http'; +import { AlertService } from 'app/core/util/alert.service'; +import { FaqCategory } from 'app/entities/faq-category.model'; +import { loadCourseFaqCategories } from 'app/faq/faq.utils'; +import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; + +@Component({ + selector: 'jhi-course-faq', + templateUrl: './course-faq.component.html', + styleUrls: ['../course-overview.scss', './course-faq.component.scss'], + encapsulation: ViewEncapsulation.None, + providers: [MetisService], + standalone: true, + imports: [ArtemisSharedComponentModule, ArtemisSharedModule, CourseFaqAccordionComponent, CustomExerciseCategoryBadgeComponent], +}) +export class CourseFaqComponent implements OnInit, OnDestroy { + private ngUnsubscribe = new Subject(); + private parentParamSubscription: Subscription; + + courseId: number; + faqs: Faq[]; + + filteredFaq: Faq[]; + existingCategories: FaqCategory[]; + activeFilters = new Set(); + + sidebarData: SidebarData; + profileSubscription?: Subscription; + isCollapsed = false; + isProduction = true; + isTestServer = false; + + @ViewChild(CourseWideSearchComponent) + courseWideSearch: CourseWideSearchComponent; + @ViewChild('courseWideSearchInput') + searchElement: ElementRef; + + courseWideSearchConfig: CourseWideSearchConfig; + courseWideSearchTerm = ''; + readonly ButtonType = ButtonType; + + // Icons + faPlus = faPlus; + faTimes = faTimes; + faFilter = faFilter; + faSearch = faSearch; + + constructor( + private route: ActivatedRoute, + private router: Router, + private faqService: FaqService, + private alertService: AlertService, + ) {} + + ngOnInit(): void { + this.parentParamSubscription = this.route.parent!.params.subscribe((params) => { + this.courseId = Number(params.courseId); + this.loadFaqs(); + this.loadCourseExerciseCategories(this.courseId); + }); + } + + private loadCourseExerciseCategories(courseId: number) { + loadCourseFaqCategories(courseId, this.alertService, this.faqService).subscribe((existingCategories) => { + this.existingCategories = existingCategories; + }); + } + + private loadFaqs() { + this.faqService + .findAllByCourseId(this.courseId) + .pipe(map((res: HttpResponse) => res.body)) + .subscribe({ + next: (res: Faq[]) => { + this.faqs = res; + this.applyFilters(); + }, + }); + } + + ngOnDestroy() { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + this.profileSubscription?.unsubscribe(); + } + + onSearch() { + this.courseWideSearchConfig.searchTerm = this.courseWideSearchTerm; + this.courseWideSearch?.onSearch(); + } + + toggleFilters(category: string) { + this.activeFilters = FaqService.toggleFilter(category, this.activeFilters); + this.applyFilters(); + } + + private applyFilters(): void { + this.filteredFaq = FaqService.applyFilters(this.activeFilters, this.faqs); + } +} diff --git a/src/main/webapp/app/overview/course-overview.component.ts b/src/main/webapp/app/overview/course-overview.component.ts index 7cf4c805712e..282563231c76 100644 --- a/src/main/webapp/app/overview/course-overview.component.ts +++ b/src/main/webapp/app/overview/course-overview.component.ts @@ -31,6 +31,7 @@ import { faListCheck, faNetworkWired, faPersonChalkboard, + faQuestion, faSync, faTable, faTimes, @@ -171,6 +172,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit faChevronRight = faChevronRight; facSidebar = facSidebar; faEllipsis = faEllipsis; + faQuestion = faQuestion; FeatureToggle = FeatureToggle; CachingStrategy = CachingStrategy; @@ -329,6 +331,15 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit sidebarItems.push(learningPathItem); } } + + if (this.course?.faqEnabled) { + const faqItem: SidebarItem = this.getFaqItem(); + sidebarItems.push(faqItem); + if (this.course?.learningPathsEnabled) { + const learningPathItem: SidebarItem = this.getLearningPathItems(); + sidebarItems.push(learningPathItem); + } + } return sidebarItems; } @@ -437,6 +448,19 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit return dashboardItem; } + getFaqItem() { + const dashboardItem: SidebarItem = { + routerLink: 'faq', + icon: faQuestion, + title: 'Faq', + translation: 'artemisApp.courseOverview.menu.faq', + hasInOrionProperty: false, + showInOrionWindow: false, + hidden: false, + }; + return dashboardItem; + } + getDefaultItems() { const items = []; if (this.course?.studentCourseAnalyticsDashboardEnabled) { diff --git a/src/main/webapp/app/overview/courses-routing.module.ts b/src/main/webapp/app/overview/courses-routing.module.ts index 8b011de2206c..a499cd6ce483 100644 --- a/src/main/webapp/app/overview/courses-routing.module.ts +++ b/src/main/webapp/app/overview/courses-routing.module.ts @@ -12,6 +12,7 @@ import { CourseTutorialGroupDetailComponent } from './tutorial-group-details/cou import { ExamParticipationComponent } from 'app/exam/participate/exam-participation.component'; import { PendingChangesGuard } from 'app/shared/guard/pending-changes.guard'; import { CourseDashboardGuard } from 'app/overview/course-dashboard/course-dashboard-guard.service'; +import { CourseFaqComponent } from 'app/overview/course-faq/course-faq.component'; const routes: Routes = [ { @@ -255,6 +256,16 @@ const routes: Routes = [ pageTitle: 'overview.plagiarismCases', }, }, + { + path: 'faq', + component: CourseFaqComponent, + data: { + authorities: [Authority.USER], + pageTitle: 'overview.faq', + hasSidebar: true, + showRefreshButton: true, + }, + }, { path: '', redirectTo: 'dashboard', // dashboard will redirect to exercises if not enabled diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index ae6d54800cd6..3585a1303bbc 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -88,7 +88,8 @@ "testExam": "Test Exam", "communication": "Communication", "plagiarismCases": "Plagiarism Cases", - "gradingSystem": "Grading System" + "gradingSystem": "Grading System", + "faq": "Faq" }, "exerciseFilter": { "filter": "Filter", From df844958ffe3bb3454855901d543227dbb03dfb5 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Mon, 16 Sep 2024 13:33:25 +0200 Subject: [PATCH 37/68] Add markdown highlighting for FAQ's --- src/main/webapp/app/faq/faq.component.html | 4 ++-- src/main/webapp/app/faq/faq.component.ts | 3 ++- .../course-faq/course-faq-accordion-component.html | 11 ++++++----- .../course-faq/course-faq-accordion-component.ts | 3 ++- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index 578240a6ccf2..cb1a1da66563 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -75,10 +75,10 @@

{{ faq.id }}

- {{ faq.questionTitle }} +

- {{ faq.questionAnswer }} +

@for (category of faq.categories; track category) { diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index 8d918768263d..1ff272c1577e 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -14,12 +14,13 @@ import { SortService } from 'app/shared/service/sort.service'; import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; @Component({ selector: 'jhi-faq', templateUrl: './faq.component.html', standalone: true, - imports: [ArtemisSharedModule, CustomExerciseCategoryBadgeComponent, ArtemisSharedComponentModule], + imports: [ArtemisSharedModule, CustomExerciseCategoryBadgeComponent, ArtemisSharedComponentModule, ArtemisMarkdownModule], }) export class FAQComponent implements OnInit, OnDestroy { faqs: Faq[]; diff --git a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html index 965712b9ec36..3e41b0eeefab 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html +++ b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html @@ -1,13 +1,14 @@ -
-
-

{{faq().questionTitle}}

+
+
+

+
@for (category of faq().categories; track category){ }
-
-

{{faq().questionAnswer}}

+
+

diff --git a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts index b3459c8aa980..08a01b290fd9 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts @@ -3,6 +3,7 @@ import { TranslateDirective } from 'app/shared/language/translate.directive'; import { Faq } from 'app/entities/faq.model'; import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; import { Subject } from 'rxjs/internal/Subject'; +import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; @Component({ selector: 'jhi-course-faq-accordion', @@ -10,7 +11,7 @@ import { Subject } from 'rxjs/internal/Subject'; styleUrl: './course-faq-accordion-component.scss', standalone: true, - imports: [TranslateDirective, CustomExerciseCategoryBadgeComponent], + imports: [TranslateDirective, CustomExerciseCategoryBadgeComponent, ArtemisMarkdownModule], }) export class CourseFaqAccordionComponent implements OnDestroy { private ngUnsubscribe = new Subject(); From 11746031dd8bd850e63cdb5802899fb176eca688 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 17 Sep 2024 13:17:15 +0200 Subject: [PATCH 38/68] Allowed students to pull stuff --- src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java | 6 +++--- src/main/webapp/i18n/de/global.json | 3 ++- src/main/webapp/i18n/en/global.json | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java index c9b961348ca1..9deb00c3d077 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java @@ -140,7 +140,7 @@ public ResponseEntity deleteFaq(@PathVariable Long faqId) { * @return the ResponseEntity with status 200 (OK) and the list of faqs in body */ @GetMapping("courses/{courseId}/faqs") - @EnforceAtLeastEditor + @EnforceAtLeastStudent public ResponseEntity> getFaqForCourse(@PathVariable Long courseId) { log.debug("REST request to get all Faqs for the course with id : {}", courseId); @@ -152,9 +152,9 @@ public ResponseEntity> getFaqForCourse(@PathVariable Long courseId) { } @GetMapping("courses/{courseId}/faq-categories") - @EnforceAtLeastEditor + @EnforceAtLeastStudent public ResponseEntity> getFaqCategoriesForCourse(@PathVariable Long courseId) { - log.debug("REST request to get all Faqs for the course with id : {}", courseId); + log.debug("REST request to get all Faq Categories for the course with id : {}", courseId); Course course = courseRepository.findByIdElseThrow(courseId); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); diff --git a/src/main/webapp/i18n/de/global.json b/src/main/webapp/i18n/de/global.json index 31fabe6283de..d5bd4ab9062b 100644 --- a/src/main/webapp/i18n/de/global.json +++ b/src/main/webapp/i18n/de/global.json @@ -346,7 +346,8 @@ "tutorialGroups": "Übungsgruppen", "statistics": "Kursstatistiken", "exams": "Klausuren", - "communication": "Kommunikation" + "communication": "Kommunikation", + "faq": "FAQ" }, "connectionStatus": { "connected": "Verbunden", diff --git a/src/main/webapp/i18n/en/global.json b/src/main/webapp/i18n/en/global.json index 18854a2515a7..ed8f81e1fbef 100644 --- a/src/main/webapp/i18n/en/global.json +++ b/src/main/webapp/i18n/en/global.json @@ -348,7 +348,8 @@ "exercises": "Exercises", "statistics": "Course statistics", "exams": "Exams", - "communication": "Communication" + "communication": "Communication", + "faq": "FAQ" }, "connectionStatus": { "connected": "Connected", From a8bd41bec876bac56e9daed661b71b3e7ccedacb Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 17 Sep 2024 14:07:04 +0200 Subject: [PATCH 39/68] fixed imports --- .../domain/Faq.java | 5 +- .../domain/FaqState.java | 2 +- .../repository/FaqRepository.java | 8 +- .../service/FaqService.java | 6 +- .../web}/FaqResource.java | 28 ++-- .../artemis/core/service/CourseService.java | 134 +++++++++--------- .../tum/in/www1/artemis/faq/FaqFactory.java | 6 +- .../www1/artemis/faq/FaqIntegrationTest.java | 10 +- 8 files changed, 104 insertions(+), 95 deletions(-) rename src/main/java/de/tum/cit/aet/artemis/{programming => communication}/domain/Faq.java (93%) rename src/main/java/de/tum/cit/aet/artemis/{programming => communication}/domain/FaqState.java (52%) rename src/main/java/de/tum/cit/aet/artemis/{programming => communication}/repository/FaqRepository.java (81%) rename src/main/java/de/tum/cit/aet/artemis/{modeling => communication}/service/FaqService.java (72%) rename src/main/java/de/tum/cit/aet/artemis/{core => communication/web}/FaqResource.java (86%) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/Faq.java similarity index 93% rename from src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java rename to src/main/java/de/tum/cit/aet/artemis/communication/domain/Faq.java index 61bb6f525526..6a2991c9a6fb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/Faq.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/Faq.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.domain; +package de.tum.cit.aet.artemis.communication.domain; import java.util.HashSet; import java.util.Set; @@ -20,6 +20,9 @@ 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. */ diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/FaqState.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/FaqState.java similarity index 52% rename from src/main/java/de/tum/cit/aet/artemis/programming/domain/FaqState.java rename to src/main/java/de/tum/cit/aet/artemis/communication/domain/FaqState.java index 7ba46b7dddb5..9018a3be3a12 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/FaqState.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/FaqState.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.domain; +package de.tum.cit.aet.artemis.communication.domain; public enum FaqState { ACCEPTED, REJECTED, PROPOSED diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/FaqRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java similarity index 81% rename from src/main/java/de/tum/cit/aet/artemis/programming/repository/FaqRepository.java rename to src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java index dd36a4940187..d1584d66fcd3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/FaqRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java @@ -1,6 +1,6 @@ -package de.tum.in.www1.artemis.repository; +package de.tum.cit.aet.artemis.communication.repository; -import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.util.Set; @@ -11,8 +11,8 @@ import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import de.tum.in.www1.artemis.domain.Faq; -import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; +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. diff --git a/src/main/java/de/tum/cit/aet/artemis/modeling/service/FaqService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java similarity index 72% rename from src/main/java/de/tum/cit/aet/artemis/modeling/service/FaqService.java rename to src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java index 0a47ed169292..bc025fb19adf 100644 --- a/src/main/java/de/tum/cit/aet/artemis/modeling/service/FaqService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java @@ -1,11 +1,11 @@ -package de.tum.in.www1.artemis.service; +package de.tum.cit.aet.artemis.communication.service; -import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import de.tum.in.www1.artemis.repository.FaqRepository; +import de.tum.cit.aet.artemis.communication.repository.FaqRepository; @Profile(PROFILE_CORE) @Service diff --git a/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java similarity index 86% rename from src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java rename to src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java index 9deb00c3d077..f41d95525c1b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java @@ -1,6 +1,6 @@ -package de.tum.in.www1.artemis.web.rest; +package de.tum.cit.aet.artemis.communication.web; -import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.net.URI; import java.net.URISyntaxException; @@ -20,18 +20,18 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import de.tum.in.www1.artemis.domain.Course; -import de.tum.in.www1.artemis.domain.Faq; -import de.tum.in.www1.artemis.repository.CourseRepository; -import de.tum.in.www1.artemis.repository.FaqRepository; -import de.tum.in.www1.artemis.security.Role; -import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastEditor; -import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; -import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; -import de.tum.in.www1.artemis.service.AuthorizationCheckService; -import de.tum.in.www1.artemis.service.FaqService; -import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; -import de.tum.in.www1.artemis.web.rest.util.HeaderUtil; +import de.tum.cit.aet.artemis.communication.domain.Faq; +import de.tum.cit.aet.artemis.communication.repository.FaqRepository; +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.repository.CourseRepository; +import de.tum.cit.aet.artemis.core.security.Role; +import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastEditor; +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. diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index a8395bb2c486..a57b8880d96b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java @@ -1,9 +1,9 @@ -package de.tum.in.www1.artemis.service; +package de.tum.cit.aet.artemis.core.service; -import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; -import static de.tum.in.www1.artemis.domain.enumeration.ComplaintType.COMPLAINT; -import static de.tum.in.www1.artemis.domain.enumeration.ComplaintType.MORE_FEEDBACK; -import static de.tum.in.www1.artemis.service.util.RoundingUtil.roundScoreSpecifiedByCourseSettings; +import static de.tum.cit.aet.artemis.assessment.domain.ComplaintType.COMPLAINT; +import static de.tum.cit.aet.artemis.assessment.domain.ComplaintType.MORE_FEEDBACK; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import static de.tum.cit.aet.artemis.core.util.RoundingUtil.roundScoreSpecifiedByCourseSettings; import java.nio.file.Files; import java.nio.file.Path; @@ -43,65 +43,71 @@ import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; -import de.tum.in.www1.artemis.config.Constants; -import de.tum.in.www1.artemis.domain.Course; -import de.tum.in.www1.artemis.domain.DomainObject; -import de.tum.in.www1.artemis.domain.Exercise; -import de.tum.in.www1.artemis.domain.GradingScale; -import de.tum.in.www1.artemis.domain.Lecture; -import de.tum.in.www1.artemis.domain.ProgrammingExercise; -import de.tum.in.www1.artemis.domain.User; -import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; -import de.tum.in.www1.artemis.domain.enumeration.NotificationType; -import de.tum.in.www1.artemis.domain.exam.Exam; -import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; -import de.tum.in.www1.artemis.domain.notification.GroupNotification; -import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismCase; -import de.tum.in.www1.artemis.domain.statistics.StatisticsEntry; -import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; -import de.tum.in.www1.artemis.repository.CompetencyRepository; -import de.tum.in.www1.artemis.repository.ComplaintRepository; -import de.tum.in.www1.artemis.repository.ComplaintResponseRepository; -import de.tum.in.www1.artemis.repository.CourseRepository; -import de.tum.in.www1.artemis.repository.ExamRepository; -import de.tum.in.www1.artemis.repository.ExerciseGroupRepository; -import de.tum.in.www1.artemis.repository.ExerciseRepository; -import de.tum.in.www1.artemis.repository.FaqRepository; -import de.tum.in.www1.artemis.repository.GradingScaleRepository; -import de.tum.in.www1.artemis.repository.GroupNotificationRepository; -import de.tum.in.www1.artemis.repository.LectureRepository; -import de.tum.in.www1.artemis.repository.ParticipantScoreRepository; -import de.tum.in.www1.artemis.repository.PrerequisiteRepository; -import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; -import de.tum.in.www1.artemis.repository.RatingRepository; -import de.tum.in.www1.artemis.repository.ResultRepository; -import de.tum.in.www1.artemis.repository.StatisticsRepository; -import de.tum.in.www1.artemis.repository.StudentParticipationRepository; -import de.tum.in.www1.artemis.repository.SubmissionRepository; -import de.tum.in.www1.artemis.repository.UserRepository; -import de.tum.in.www1.artemis.repository.metis.conversation.ConversationRepository; -import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismCaseRepository; -import de.tum.in.www1.artemis.repository.tutorialgroups.TutorialGroupNotificationRepository; -import de.tum.in.www1.artemis.repository.tutorialgroups.TutorialGroupRepository; -import de.tum.in.www1.artemis.security.Role; -import de.tum.in.www1.artemis.security.SecurityUtils; -import de.tum.in.www1.artemis.service.dto.StudentDTO; -import de.tum.in.www1.artemis.service.exam.ExamDeletionService; -import de.tum.in.www1.artemis.service.export.CourseExamExportService; -import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; -import de.tum.in.www1.artemis.service.learningpath.LearningPathService; -import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; -import de.tum.in.www1.artemis.service.tutorialgroups.TutorialGroupChannelManagementService; -import de.tum.in.www1.artemis.service.user.UserService; -import de.tum.in.www1.artemis.service.util.TimeLogUtil; -import de.tum.in.www1.artemis.web.rest.dto.CourseContentCount; -import de.tum.in.www1.artemis.web.rest.dto.CourseManagementDetailViewDTO; -import de.tum.in.www1.artemis.web.rest.dto.DueDateStat; -import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; -import de.tum.in.www1.artemis.web.rest.dto.StatsForDashboardDTO; -import de.tum.in.www1.artemis.web.rest.dto.TutorLeaderboardDTO; -import de.tum.in.www1.artemis.web.rest.dto.pageablesearch.SearchTermPageableSearchDTO; -import de.tum.in.www1.artemis.web.rest.util.PageUtil; +import de.tum.cit.aet.artemis.assessment.domain.GradingScale; +import de.tum.cit.aet.artemis.assessment.repository.ComplaintRepository; +import de.tum.cit.aet.artemis.assessment.repository.ComplaintResponseRepository; +import de.tum.cit.aet.artemis.assessment.repository.GradingScaleRepository; +import de.tum.cit.aet.artemis.assessment.repository.ParticipantScoreRepository; +import de.tum.cit.aet.artemis.assessment.repository.RatingRepository; +import de.tum.cit.aet.artemis.assessment.repository.ResultRepository; +import de.tum.cit.aet.artemis.assessment.service.ComplaintService; +import de.tum.cit.aet.artemis.assessment.service.PresentationPointsCalculationService; +import de.tum.cit.aet.artemis.assessment.service.TutorLeaderboardService; +import de.tum.cit.aet.artemis.atlas.repository.CompetencyRelationRepository; +import de.tum.cit.aet.artemis.atlas.repository.CompetencyRepository; +import de.tum.cit.aet.artemis.atlas.repository.PrerequisiteRepository; +import de.tum.cit.aet.artemis.atlas.service.learningpath.LearningPathService; +import de.tum.cit.aet.artemis.communication.domain.NotificationType; +import de.tum.cit.aet.artemis.communication.domain.notification.GroupNotification; +import de.tum.cit.aet.artemis.communication.repository.FaqRepository; +import de.tum.cit.aet.artemis.communication.repository.GroupNotificationRepository; +import de.tum.cit.aet.artemis.communication.repository.conversation.ConversationRepository; +import de.tum.cit.aet.artemis.communication.service.notifications.GroupNotificationService; +import de.tum.cit.aet.artemis.core.config.Constants; +import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.core.domain.DomainObject; +import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.dto.CourseContentCount; +import de.tum.cit.aet.artemis.core.dto.CourseManagementDetailViewDTO; +import de.tum.cit.aet.artemis.core.dto.DueDateStat; +import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; +import de.tum.cit.aet.artemis.core.dto.StatisticsEntry; +import de.tum.cit.aet.artemis.core.dto.StatsForDashboardDTO; +import de.tum.cit.aet.artemis.core.dto.StudentDTO; +import de.tum.cit.aet.artemis.core.dto.TutorLeaderboardDTO; +import de.tum.cit.aet.artemis.core.dto.pageablesearch.SearchTermPageableSearchDTO; +import de.tum.cit.aet.artemis.core.repository.CourseRepository; +import de.tum.cit.aet.artemis.core.repository.StatisticsRepository; +import de.tum.cit.aet.artemis.core.repository.UserRepository; +import de.tum.cit.aet.artemis.core.security.Role; +import de.tum.cit.aet.artemis.core.security.SecurityUtils; +import de.tum.cit.aet.artemis.core.service.export.CourseExamExportService; +import de.tum.cit.aet.artemis.core.service.user.UserService; +import de.tum.cit.aet.artemis.core.util.PageUtil; +import de.tum.cit.aet.artemis.core.util.TimeLogUtil; +import de.tum.cit.aet.artemis.exam.domain.Exam; +import de.tum.cit.aet.artemis.exam.domain.ExerciseGroup; +import de.tum.cit.aet.artemis.exam.repository.ExamRepository; +import de.tum.cit.aet.artemis.exam.repository.ExerciseGroupRepository; +import de.tum.cit.aet.artemis.exam.service.ExamDeletionService; +import de.tum.cit.aet.artemis.exercise.domain.Exercise; +import de.tum.cit.aet.artemis.exercise.domain.IncludedInOverallScore; +import de.tum.cit.aet.artemis.exercise.repository.ExerciseRepository; +import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository; +import de.tum.cit.aet.artemis.exercise.repository.SubmissionRepository; +import de.tum.cit.aet.artemis.exercise.service.ExerciseDeletionService; +import de.tum.cit.aet.artemis.exercise.service.ExerciseService; +import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService; +import de.tum.cit.aet.artemis.lecture.domain.Lecture; +import de.tum.cit.aet.artemis.lecture.repository.LectureRepository; +import de.tum.cit.aet.artemis.lecture.service.LectureService; +import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismCase; +import de.tum.cit.aet.artemis.plagiarism.repository.PlagiarismCaseRepository; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; +import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; +import de.tum.cit.aet.artemis.tutorialgroup.repository.TutorialGroupNotificationRepository; +import de.tum.cit.aet.artemis.tutorialgroup.repository.TutorialGroupRepository; +import de.tum.cit.aet.artemis.tutorialgroup.service.TutorialGroupChannelManagementService; /** * Service Implementation for managing Course. diff --git a/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java b/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java index 815940598395..2a32a97fe090 100644 --- a/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java +++ b/src/test/java/de/tum/in/www1/artemis/faq/FaqFactory.java @@ -3,9 +3,9 @@ import java.util.HashSet; import java.util.Set; -import de.tum.in.www1.artemis.domain.Course; -import de.tum.in.www1.artemis.domain.Faq; -import de.tum.in.www1.artemis.domain.FaqState; +import de.tum.cit.aet.artemis.communication.domain.Faq; +import de.tum.cit.aet.artemis.communication.domain.FaqState; +import de.tum.cit.aet.artemis.core.domain.Course; public class FaqFactory { diff --git a/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java index b3f62be75e70..7b756d08bff7 100644 --- a/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/faq/FaqIntegrationTest.java @@ -12,11 +12,11 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; -import de.tum.in.www1.artemis.domain.Course; -import de.tum.in.www1.artemis.domain.Faq; -import de.tum.in.www1.artemis.domain.FaqState; -import de.tum.in.www1.artemis.repository.FaqRepository; +import de.tum.cit.aet.artemis.AbstractSpringIntegrationIndependentTest; +import de.tum.cit.aet.artemis.communication.domain.Faq; +import de.tum.cit.aet.artemis.communication.domain.FaqState; +import de.tum.cit.aet.artemis.communication.repository.FaqRepository; +import de.tum.cit.aet.artemis.core.domain.Course; class FaqIntegrationTest extends AbstractSpringIntegrationIndependentTest { From 688ceb86413cf211cf5e165b62523a50bedeecb4 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 17 Sep 2024 14:36:53 +0200 Subject: [PATCH 40/68] moved faq button up --- .../course/manage/course-update.component.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/webapp/app/course/manage/course-update.component.html b/src/main/webapp/app/course/manage/course-update.component.html index 5af60571f1fd..e8c0afe0db4e 100644 --- a/src/main/webapp/app/course/manage/course-update.component.html +++ b/src/main/webapp/app/course/manage/course-update.component.html @@ -309,6 +309,15 @@
}
+
+ + + +
-
- - - -
@if (this.isAdmin) {
From 0290f9b000a1746a95488e1ec9dc7f3634072319 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 17 Sep 2024 14:47:08 +0200 Subject: [PATCH 41/68] Made page scrollable, moved categories in the same row to safe space --- src/main/webapp/app/faq/faq.component.html | 8 +++++--- .../overview/course-faq/course-faq.component.html | 12 +++++++----- .../overview/course-faq/course-faq.component.scss | 6 ++++++ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index cb1a1da66563..2a82a027ad3d 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -81,9 +81,11 @@

- @for (category of faq.categories; track category) { - - } +
+ @for (category of faq.categories; track category) { + + } +
diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.html b/src/main/webapp/app/overview/course-faq/course-faq.component.html index b165b4145339..6ca7d8ce43fe 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.html +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.html @@ -35,9 +35,11 @@ - @for (faq of this.filteredFaq; track faq) { -
- -
- } +
+ @for (faq of this.filteredFaq; track faq) { +
+ +
+ } +
diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.scss b/src/main/webapp/app/overview/course-faq/course-faq.component.scss index 9e1c700ded25..25093ce4e1ff 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.scss +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.scss @@ -5,3 +5,9 @@ .module-bg { background-color: var(--module-bg); } + +.scroll-container { + max-height: 80vh; + overflow-y: auto; + overflow-x: hidden; +} From 5976d6fb6b30baf48c7e01827c07ae634e2eada8 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 17 Sep 2024 14:52:16 +0200 Subject: [PATCH 42/68] removed search bar --- .../overview/course-faq/course-faq.component.html | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.html b/src/main/webapp/app/overview/course-faq/course-faq.component.html index 6ca7d8ce43fe..475627030998 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.html +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.html @@ -1,15 +1,5 @@
-
- - +
From 047eb52a867478e0aa352ae9d6a2c9559e2c01c6 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 17 Sep 2024 16:05:44 +0200 Subject: [PATCH 43/68] Fixed style issues --- .../communication/web/FaqResource.java | 6 +++++ .../course-management-tab-bar.component.ts | 4 ++-- .../course-management-card.component.ts | 4 ++-- src/main/webapp/app/entities/course.model.ts | 2 +- src/main/webapp/app/entities/faq.model.ts | 22 ++++++++----------- src/main/webapp/app/faq/faq.routes.ts | 4 ---- .../course-faq-accordion-component.scss | 3 +-- .../webapp/i18n/en/student-dashboard.json | 2 +- 8 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java index f41d95525c1b..17b6fc2f4332 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java @@ -151,6 +151,12 @@ public ResponseEntity> getFaqForCourse(@PathVariable Long 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 + */ @GetMapping("courses/{courseId}/faq-categories") @EnforceAtLeastStudent public ResponseEntity> getFaqCategoriesForCourse(@PathVariable Long courseId) { diff --git a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts index bf4cf8452999..c25c182067e3 100644 --- a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts +++ b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts @@ -20,12 +20,12 @@ import { faNetworkWired, faPersonChalkboard, faPuzzlePiece, + faQuestion, faRobot, faTable, faTrash, faUserCheck, faWrench, - faQuestion } from '@fortawesome/free-solid-svg-icons'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; import { CourseAdminService } from 'app/course/manage/course-admin.service'; @@ -74,7 +74,7 @@ export class CourseManagementTabBarComponent implements OnInit, OnDestroy, After faRobot = faRobot; faPuzzlePiece = faPuzzlePiece; faList = faList; - faQuestion = faQuestion + faQuestion = faQuestion; isCommunicationEnabled = false; diff --git a/src/main/webapp/app/course/manage/overview/course-management-card.component.ts b/src/main/webapp/app/course/manage/overview/course-management-card.component.ts index e5fdaec28a2e..2fd25af861ed 100644 --- a/src/main/webapp/app/course/manage/overview/course-management-card.component.ts +++ b/src/main/webapp/app/course/manage/overview/course-management-card.component.ts @@ -19,10 +19,10 @@ import { faListAlt, faNetworkWired, faPersonChalkboard, + faQuestion, faSpinner, faTable, faUserCheck, - faQuestion } from '@fortawesome/free-solid-svg-icons'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; @@ -78,7 +78,7 @@ export class CourseManagementCardComponent implements OnChanges { faAngleUp = faAngleUp; faPersonChalkboard = faPersonChalkboard; faSpinner = faSpinner; - faQuestion = faQuestion + faQuestion = faQuestion; courseColor: string; diff --git a/src/main/webapp/app/entities/course.model.ts b/src/main/webapp/app/entities/course.model.ts index cd61cefdec33..6cddcfe61040 100644 --- a/src/main/webapp/app/entities/course.model.ts +++ b/src/main/webapp/app/entities/course.model.ts @@ -62,7 +62,7 @@ export class Course implements BaseEntity { public color?: string; public courseIcon?: string; public onlineCourse?: boolean; - public faqEnabled?: boolean + public faqEnabled?: boolean; public enrollmentEnabled?: boolean; public enrollmentConfirmationMessage?: string; public unenrollmentEnabled?: boolean; diff --git a/src/main/webapp/app/entities/faq.model.ts b/src/main/webapp/app/entities/faq.model.ts index ea28d55b5c5f..f71748c89c66 100644 --- a/src/main/webapp/app/entities/faq.model.ts +++ b/src/main/webapp/app/entities/faq.model.ts @@ -1,24 +1,20 @@ import { BaseEntity } from 'app/shared/model/base-entity'; import { Course } from 'app/entities/course.model'; -import {FaqCategory} from "app/entities/faq-category.model"; +import { FaqCategory } from './faq-category.model'; -export enum FaqState{ - ACCEPTED, REJECTED, PROPOSED +export enum FaqState { + ACCEPTED, + REJECTED, + PROPOSED, } export class Faq implements BaseEntity { public id?: number; public questionTitle?: string; public questionAnswer?: string; - public faqState? : FaqState - public course? : Course - public categories?: FaqCategory[] + public faqState?: FaqState; + public course?: Course; + public categories?: FaqCategory[]; - // - isAtLeastEditor?: boolean; - isAtLeastInstructor?: boolean; - - - constructor() { - } + constructor() {} } diff --git a/src/main/webapp/app/faq/faq.routes.ts b/src/main/webapp/app/faq/faq.routes.ts index ed772543c6d6..022c836d157c 100644 --- a/src/main/webapp/app/faq/faq.routes.ts +++ b/src/main/webapp/app/faq/faq.routes.ts @@ -12,7 +12,6 @@ import { FaqService } from 'app/faq/faq.service'; import { Faq } from 'app/entities/faq.model'; import { FAQUpdateComponent } from 'app/faq/faq-update.component'; - @Injectable({ providedIn: 'root' }) export class FAQResolve implements Resolve { constructor(private faqService: FaqService) {} @@ -29,7 +28,6 @@ export class FAQResolve implements Resolve { } } - export const faqRoutes: Routes = [ { path: ':courseId/faqs', @@ -60,7 +58,6 @@ export const faqRoutes: Routes = [ data: { authorities: [Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], pageTitle: 'global.generic.create', - }, canActivate: [UserRouteAccessService], }, @@ -79,7 +76,6 @@ export const faqRoutes: Routes = [ }, canActivate: [UserRouteAccessService], }, - ], }, ], diff --git a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.scss b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.scss index fe52693ae074..0db97794d8c8 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.scss +++ b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.scss @@ -3,11 +3,10 @@ justify-content: space-between; align-items: center; width: 100%; - padding: 10px; box-sizing: border-box; } -.faq-container h1 { +.faq-container h2 { margin: 0; } diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 3585a1303bbc..7d13035e00d4 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -89,7 +89,7 @@ "communication": "Communication", "plagiarismCases": "Plagiarism Cases", "gradingSystem": "Grading System", - "faq": "Faq" + "faq": "FAQ" }, "exerciseFilter": { "filter": "Filter", From 5114dd853b34190c2d8f74f1174f246099f0ed91 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 17 Sep 2024 16:39:23 +0200 Subject: [PATCH 44/68] Fixed coderabbit style issues --- .../communication/web/FaqResource.java | 13 +- .../cit/aet/artemis/core/domain/Course.java | 2 +- .../manage/course-update.component.html | 2 +- .../course/manage/course-update.component.ts | 2 - .../webapp/app/faq/faq-update.component.html | 76 +++++---- src/main/webapp/app/faq/faq.component.html | 144 +++++++++--------- .../course-faq/course-faq.component.html | 2 - .../app/overview/course-overview.component.ts | 2 +- 8 files changed, 115 insertions(+), 128 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java index 17b6fc2f4332..6b6bc74fe6b9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java @@ -27,7 +27,6 @@ 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.EnforceAtLeastEditor; 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; @@ -72,13 +71,13 @@ public FaqResource(FaqRepository faqRepository, FaqService faqService, CourseRep * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("faqs") - @EnforceAtLeastEditor + @EnforceAtLeastInstructor public ResponseEntity 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.EDITOR, faq.getCourse(), null); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); Faq savedFaq = faqRepository.save(faq); return ResponseEntity.created(new URI("/api/faqs/" + savedFaq.getId())).body(savedFaq); @@ -92,13 +91,13 @@ public ResponseEntity createFaq(@RequestBody Faq faq) throws URISyntaxExcep * Server Error) if the faq couldn't be updated */ @PutMapping("faqs/{faqId}") - @EnforceAtLeastEditor + @EnforceAtLeastInstructor public ResponseEntity updateFaq(@RequestBody Faq faq) { log.debug("REST request to update Faq : {}", faq); if (faq.getId() == null) { throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idNull"); } - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, faq.getCourse(), null); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); Faq result = faqRepository.save(faq); return ResponseEntity.ok().body(result); } @@ -145,7 +144,7 @@ public ResponseEntity> 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.EDITOR, course, null); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); Set faqs = faqRepository.findAllByCourseId(courseId); return ResponseEntity.ok().body(faqs); @@ -163,7 +162,7 @@ public ResponseEntity> getFaqCategoriesForCourse(@PathVariable Long log.debug("REST request to get all Faq Categories for the course with id : {}", courseId); Course course = courseRepository.findByIdElseThrow(courseId); - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); Set faqs = faqRepository.findAllCategoriesByCourseId(courseId); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java b/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java index ad59da8239a0..0b03d89cf17c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java @@ -189,7 +189,7 @@ public class Course extends DomainObject { private boolean unenrollmentEnabled = false; @Column(name = "faq_enabled") - private boolean faqEnabled = false; + private Boolean faqEnabled = false; @Column(name = "presentation_score") private Integer presentationScore; diff --git a/src/main/webapp/app/course/manage/course-update.component.html b/src/main/webapp/app/course/manage/course-update.component.html index e8c0afe0db4e..7010ac06bd55 100644 --- a/src/main/webapp/app/course/manage/course-update.component.html +++ b/src/main/webapp/app/course/manage/course-update.component.html @@ -311,7 +311,7 @@
- +
- @if (true) { -
-
-
-
-

-
+ +
+
+
+

+
+
+
+
+
+ +
+
-
+
+ + +
+
+ + + +
+ @if (faq.course) {
- +
- +
-
- - -
-
- - - -
- @if (faq.course) { -
- -
- -
-
- } -
-
- - -
+ } +
+
+ +
-
- - - } +
+
diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index 2a82a027ad3d..bb42f5c4b6fe 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -44,80 +44,76 @@


- @if (true) { -
- - - - - - - - - - - - @for (faq of filteredFaq; track trackId(i, faq); let i = $index) { - - - - - - + + } + +
- - - - - - - - - - - -
- {{ faq.id }} - -

-
-

-
-
- @for (category of faq.categories; track category) { - - } -
-
-
-
- @if (true) { - - - - - } - @if (true) { - - } -
+
+ + + + + + + + + + + + @for (faq of filteredFaq; track trackId(i, faq); let i = $index) { + + + + + + + - - } - -
+ + + + + + + + + + + +
+ {{ faq.id }} + +

+
+

+
+
+ @for (category of faq.categories; track category) { + + } +
+
+
+
+ + + + + +
-
-
- } +
+
+
diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.html b/src/main/webapp/app/overview/course-faq/course-faq.component.html index 475627030998..0e75e73f6a75 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.html +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.html @@ -1,7 +1,5 @@
- -
@if (faq.course) {
diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index 5cd81aa80fd7..ed7611f4ca5c 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -30,7 +30,7 @@ export class FAQUpdateComponent implements OnInit { faq: Faq; isSaving: boolean; existingCategories: FaqCategory[] = []; - exerciseCategories: FaqCategory[] = []; + faqCategories: FaqCategory[] = []; courses: Course[]; @@ -67,7 +67,7 @@ export class FAQUpdateComponent implements OnInit { this.loadCourseFaqCategories(course.id); } if (faq.categories) { - this.exerciseCategories = faq.categories; + this.faqCategories = faq.categories; } }); } @@ -139,7 +139,7 @@ export class FAQUpdateComponent implements OnInit { updateCategories(categories: FaqCategory[]) { this.faq.categories = categories; - this.exerciseCategories = categories; + this.faqCategories = categories; } private loadCourseFaqCategories(courseId: number) { diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index 5d8ddbf9cd08..416649ee8dbb 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -15,7 +15,7 @@

id="filter-dropdown-button" > - +
    @for (category of existingCategories; track category) { @@ -28,14 +28,14 @@

    [checked]="activeFilters.has(category.category!)" type="checkbox" /> - + }

- + @@ -81,7 +81,7 @@

-
+
@for (category of faq.categories; track category) { } @@ -99,7 +99,7 @@

- + diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index a15c4085c185..825fb5e0f979 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -67,7 +67,7 @@ export class FAQComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { - this.dialogErrorSource.unsubscribe(); + this.dialogErrorSource.complete(); } trackId(index: number, item: Faq) { diff --git a/src/main/webapp/app/faq/faq.utils.ts b/src/main/webapp/app/faq/faq.utils.ts index 0c6808fbdb60..f96bffdbb575 100644 --- a/src/main/webapp/app/faq/faq.utils.ts +++ b/src/main/webapp/app/faq/faq.utils.ts @@ -1,29 +1,23 @@ import { onError } from 'app/shared/util/global.utils'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; -import { Observable } from 'rxjs'; -import { ExerciseCategory } from 'app/entities/exercise-category.model'; +import { Observable, catchError, map, of } from 'rxjs'; import { FaqService } from 'app/faq/faq.service'; import { FaqCategory } from 'app/entities/faq-category.model'; export function loadCourseFaqCategories(courseId: number | undefined, alertService: AlertService, faqService: FaqService): Observable { if (courseId === undefined) { - return new Observable((observer) => { - observer.complete(); - }); + return of([]); } - return new Observable((observer) => { - faqService.findAllCategoriesByCourseId(courseId).subscribe({ - next: (categoryRes: HttpResponse) => { - const existingCategories = faqService.convertFaqCategoriesAsStringFromServer(categoryRes.body!); - observer.next(existingCategories); - observer.complete(); - }, - error: (error: HttpErrorResponse) => { - onError(alertService, error); - observer.complete(); - }, - }); - }); + return faqService.findAllCategoriesByCourseId(courseId).pipe( + map((categoryRes: HttpResponse) => { + const existingCategories = faqService.convertFaqCategoriesAsStringFromServer(categoryRes.body || []); + return existingCategories; + }), + catchError((error: HttpErrorResponse) => { + onError(alertService, error); + return of([]); + }), + ); } diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.html b/src/main/webapp/app/overview/course-faq/course-faq.component.html index a02c3ceb6542..86bfad84662b 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.html +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.html @@ -25,7 +25,7 @@
@for (faq of this.filteredFaq; track faq) { -
+
} diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.scss b/src/main/webapp/app/overview/course-faq/course-faq.component.scss index 9907e9aebf82..25093ce4e1ff 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.scss +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.scss @@ -11,8 +11,3 @@ overflow-y: auto; overflow-x: hidden; } - -.category-badge { - margin-top: 10px; - margin-left: 4px; -} diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts index 964ba0668b41..b502280f2c6f 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -21,7 +21,7 @@ import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-catego @Component({ selector: 'jhi-course-faq', templateUrl: './course-faq.component.html', - styleUrls: ['../course-overview.scss', './course-faq.component.scss'], + styleUrls: ['../course-overview.scss', './course-faq.component.scss', '../../faq/faq.component.scss'], encapsulation: ViewEncapsulation.None, providers: [MetisService], standalone: true, diff --git a/src/test/java/de/tum/cit/aet/artemis/FaqFactory.java b/src/test/java/de/tum/cit/aet/artemis/FaqFactory.java index d985355af579..92b23c833cdb 100644 --- a/src/test/java/de/tum/cit/aet/artemis/FaqFactory.java +++ b/src/test/java/de/tum/cit/aet/artemis/FaqFactory.java @@ -9,12 +9,12 @@ public class FaqFactory { - public static Faq generateFaq(Course course) { + public static Faq generateFaq(Course course, FaqState state, String title, String answer) { Faq faq = new Faq(); faq.setCourse(course); - faq.setFaqState(FaqState.ACCEPTED); - faq.setQuestionAnswer("Answer"); - faq.setQuestionTitle("Title"); + faq.setFaqState(state); + faq.setQuestionTitle(title); + faq.setQuestionAnswer(answer); faq.setCategories(generateFaqCategories()); return faq; } diff --git a/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java index fa320a722298..61c373619fa7 100644 --- a/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java @@ -34,7 +34,7 @@ void initTestCase() throws Exception { userUtilService.addUsers(TEST_PREFIX, 1, numberOfTutors, 0, 1); List courses = courseUtilService.createCoursesWithExercisesAndLectures(TEST_PREFIX, true, true, numberOfTutors); this.course1 = this.courseRepository.findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(courses.getFirst().getId()); - this.faq = FaqFactory.generateFaq(course1); + this.faq = FaqFactory.generateFaq(course1, FaqState.ACCEPTED, "answer", "title"); faqRepository.save(this.faq); // Add users that are not in the course userUtilService.createAndSaveUser(TEST_PREFIX + "student42"); @@ -64,7 +64,7 @@ void testAll_asStudent() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createFaq_correctRequestBody_shouldCreateFaq() throws Exception { - Faq newFaq = FaqFactory.generateFaq(course1); + Faq newFaq = FaqFactory.generateFaq(course1, FaqState.ACCEPTED, "title", "answer"); Faq returnedFaq = request.postWithResponseBody("/api/faqs", newFaq, Faq.class, HttpStatus.CREATED); assertThat(returnedFaq).isNotNull(); assertThat(returnedFaq.getId()).isNotNull(); @@ -99,6 +99,8 @@ void updateFaq_correctRequestBody_shouldUpdateFaq() throws Exception { assertThat(updatedFaq.getQuestionAnswer()).isEqualTo("Update"); assertThat(updatedFaq.getFaqState()).isEqualTo(FaqState.PROPOSED); assertThat(updatedFaq.getCategories()).isEqualTo(newCategories); + assertThat(updatedFaq.getCreatedDate()).isNotNull(); + assertThat(updatedFaq.getLastModifiedDate()).isNotNull(); } @Test From 5aeda2fb7c9b6939644c555fccbaa933f867b24b Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 18 Sep 2024 12:35:51 +0200 Subject: [PATCH 49/68] Further Coderabbit --- src/main/webapp/app/faq/faq-update.component.ts | 7 ++++--- src/main/webapp/app/faq/faq.component.html | 6 ++++-- src/main/webapp/app/faq/faq.component.ts | 4 ++-- .../webapp/app/overview/course-faq/course-faq.component.ts | 3 +-- src/main/webapp/i18n/de/faq.json | 6 +++--- src/main/webapp/i18n/en/faq.json | 6 +++--- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index 1968e7d4218d..b3c1073f538a 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -11,7 +11,7 @@ import { faBan, faQuestionCircle, faSave } from '@fortawesome/free-solid-svg-ico import { FormulaAction } from 'app/shared/monaco-editor/model/actions/formula.action'; import { Faq } from 'app/entities/faq.model'; import { FaqService } from 'app/faq/faq.service'; - +import { TranslateService } from '@ngx-translate/core'; import { FaqCategory } from 'app/entities/faq-category.model'; import { loadCourseFaqCategories } from 'app/faq/faq.utils'; import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; @@ -50,6 +50,7 @@ export class FAQUpdateComponent implements OnInit { protected activatedRoute: ActivatedRoute, private navigationUtilService: ArtemisNavigationUtilService, private router: Router, + private translateService: TranslateService, ) {} /** @@ -117,13 +118,13 @@ export class FAQUpdateComponent implements OnInit { if (faqBody) { this.faq = faqBody; } - this.alertService.success(`FAQ with title ${faq.questionTitle} was successfully created.`); + this.alertService.success(this.translateService.instant('artemisApp.faq.created', { id: faq.id })); this.router.navigate(['course-management', faq.course!.id, 'faqs']); }, }); } else { this.isSaving = false; - this.alertService.success(`FAQ with title ${faq.questionTitle} was successfully updated.`); + this.alertService.success(this.translateService.instant('artemisApp.faq.updated', { id: faq.id })); this.router.navigate(['course-management', faq.course!.id, 'faqs']); } } diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index 60d9ef31ccd2..2f9e0a7d1ca6 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -35,7 +35,7 @@

- + @@ -68,7 +68,7 @@

@@ -90,6 +90,8 @@

+ b +
diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index 825fb5e0f979..0c86861bfd4b 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -63,7 +63,7 @@ export class FAQComponent implements OnInit, OnDestroy { ngOnInit() { this.courseId = Number(this.route.snapshot.paramMap.get('courseId')); this.loadAll(); - this.loadCourseExerciseCategories(this.courseId); + this.loadCourseFaqCategories(this.courseId); } ngOnDestroy(): void { @@ -113,7 +113,7 @@ export class FAQComponent implements OnInit, OnDestroy { }); } - private loadCourseExerciseCategories(courseId: number) { + private loadCourseFaqCategories(courseId: number) { loadCourseFaqCategories(courseId, this.alertService, this.faqService).subscribe((existingCategories) => { this.existingCategories = existingCategories; }); diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts index b502280f2c6f..f172f3d7fee0 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -39,7 +39,6 @@ export class CourseFaqComponent implements OnInit, OnDestroy { activeFilters = new Set(); sidebarData: SidebarData; - profileSubscription?: Subscription; isCollapsed = false; isProduction = true; isTestServer = false; @@ -95,7 +94,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy { ngOnDestroy() { this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); - this.profileSubscription?.unsubscribe(); + this.parentParamSubscription?.unsubscribe(); } onSearch() { diff --git a/src/main/webapp/i18n/de/faq.json b/src/main/webapp/i18n/de/faq.json index 585a84a147d3..3d577eda8797 100644 --- a/src/main/webapp/i18n/de/faq.json +++ b/src/main/webapp/i18n/de/faq.json @@ -7,9 +7,9 @@ "filterLabel": "Filter", "createOrEditLabel": "FAQ erstellen oder bearbeiten" }, - "created": "FAQ erstellt mit ID {{ param }}", - "updated": "FAQ aktualisiert mit ID {{ param }}", - "deleted": "FAQ gelöscht mit ID {{ param }}", + "created": "Das FAQ wurde erfoglreich erstellt", + "updated": "Das FAQ wurde erfolgreich aktualisiert", + "deleted": "Das FAQ wurde erfolgreich gelöscht", "delete": { "question": "Soll das FAQ {{ title }} wirklich dauerhaft gelöscht werden? Diese Aktion kann NICHT rückgängig gemacht werden!", "typeNameToConfirm": "Bitte gib den Namen des FAQ zur Bestätigung ein." diff --git a/src/main/webapp/i18n/en/faq.json b/src/main/webapp/i18n/en/faq.json index 26567d8c3927..c0808c2f4dea 100644 --- a/src/main/webapp/i18n/en/faq.json +++ b/src/main/webapp/i18n/en/faq.json @@ -7,9 +7,9 @@ "filterLabel": "Filter", "createOrEditLabel": "FAQ erstellen oder bearbeiten" }, - "created": "Created new FAQ with identifier {{ param }}", - "updated": "Updated FAQ with identifier {{ param }}", - "deleted": "Deleted FAQ with identifier {{ param }}", + "created": "The FAQ was successfully created", + "updated": "The FAQ was successfully updated", + "deleted": "The FAQ was successfully deleted", "delete": { "question": "Are you sure you want to permanently delete the FAQ {{ title }}? This action can NOT be undone!", "typeNameToConfirm": "Please type in the name of the FAQ to confirm." From f5ab0e47777ce53bed1c2781cf990a35121650ce Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 18 Sep 2024 13:36:27 +0200 Subject: [PATCH 50/68] improved validation and further coderabbit fixes --- .../webapp/app/faq/faq-update.component.ts | 5 ++++- src/main/webapp/app/faq/faq.component.html | 2 -- .../course-faq/course-faq.component.ts | 22 +++++-------------- src/main/webapp/i18n/de/faq.json | 2 +- src/main/webapp/i18n/en/faq.json | 2 +- 5 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index b3c1073f538a..3b3b5d2c61c2 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -154,6 +154,9 @@ export class FAQUpdateComponent implements OnInit { } canSave() { - return this.faq.questionTitle && this.faq.questionAnswer; + if (this.faq.questionTitle && this.faq.questionAnswer) { + return this.faq.questionTitle?.trim().length > 0 && this.faq.questionAnswer?.trim().length > 0; + } + return false; } } diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index 2f9e0a7d1ca6..8783e54e9aa0 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -90,8 +90,6 @@

- b -
diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts index f172f3d7fee0..2bad9daf064c 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -1,22 +1,22 @@ -import { Component, ElementRef, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { map } from 'rxjs/operators'; import { Subject, Subscription } from 'rxjs'; import { MetisService } from 'app/shared/metis/metis.service'; -import { faFilter, faPlus, faSearch, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { faFilter, faPlus, faTimes } from '@fortawesome/free-solid-svg-icons'; import { ButtonType } from 'app/shared/components/button.component'; -import { CourseWideSearchComponent, CourseWideSearchConfig } from 'app/overview/course-conversations/course-wide-search/course-wide-search.component'; import { SidebarData } from 'app/types/sidebar'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { CourseFaqAccordionComponent } from 'app/overview/course-faq/course-faq-accordion-component'; import { Faq } from 'app/entities/faq.model'; import { FaqService } from 'app/faq/faq.service'; -import { HttpResponse } from '@angular/common/http'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; import { FaqCategory } from 'app/entities/faq-category.model'; import { loadCourseFaqCategories } from 'app/faq/faq.utils'; import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; +import { onError } from 'app/shared/util/global.utils'; @Component({ selector: 'jhi-course-faq', @@ -43,20 +43,12 @@ export class CourseFaqComponent implements OnInit, OnDestroy { isProduction = true; isTestServer = false; - @ViewChild(CourseWideSearchComponent) - courseWideSearch: CourseWideSearchComponent; - @ViewChild('courseWideSearchInput') - searchElement: ElementRef; - - courseWideSearchConfig: CourseWideSearchConfig; - courseWideSearchTerm = ''; readonly ButtonType = ButtonType; // Icons faPlus = faPlus; faTimes = faTimes; faFilter = faFilter; - faSearch = faSearch; constructor( private route: ActivatedRoute, @@ -88,6 +80,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy { this.faqs = res; this.applyFilters(); }, + error: (res: HttpErrorResponse) => onError(this.alertService, res), }); } @@ -97,11 +90,6 @@ export class CourseFaqComponent implements OnInit, OnDestroy { this.parentParamSubscription?.unsubscribe(); } - onSearch() { - this.courseWideSearchConfig.searchTerm = this.courseWideSearchTerm; - this.courseWideSearch?.onSearch(); - } - toggleFilters(category: string) { this.activeFilters = FaqService.toggleFilter(category, this.activeFilters); this.applyFilters(); diff --git a/src/main/webapp/i18n/de/faq.json b/src/main/webapp/i18n/de/faq.json index 3d577eda8797..7b8b2aa90c97 100644 --- a/src/main/webapp/i18n/de/faq.json +++ b/src/main/webapp/i18n/de/faq.json @@ -7,7 +7,7 @@ "filterLabel": "Filter", "createOrEditLabel": "FAQ erstellen oder bearbeiten" }, - "created": "Das FAQ wurde erfoglreich erstellt", + "created": "Das FAQ wurde erfolgreich erstellt", "updated": "Das FAQ wurde erfolgreich aktualisiert", "deleted": "Das FAQ wurde erfolgreich gelöscht", "delete": { diff --git a/src/main/webapp/i18n/en/faq.json b/src/main/webapp/i18n/en/faq.json index c0808c2f4dea..1a158eb52c40 100644 --- a/src/main/webapp/i18n/en/faq.json +++ b/src/main/webapp/i18n/en/faq.json @@ -5,7 +5,7 @@ "title": "FAQ", "createLabel": "Create a new FAQ", "filterLabel": "Filter", - "createOrEditLabel": "FAQ erstellen oder bearbeiten" + "createOrEditLabel": "Create or edit FAQ" }, "created": "The FAQ was successfully created", "updated": "The FAQ was successfully updated", From b9f0935a60de314293aa9e894fcd0825e79e9eb0 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 18 Sep 2024 14:24:38 +0200 Subject: [PATCH 51/68] Fixed minor issue --- .../communication/service/FaqService.java | 5 +++ .../communication/web/FaqResource.java | 31 +++++++++++++------ .../course/manage/course-update.component.ts | 2 +- .../webapp/app/faq/faq-update.component.html | 4 +-- .../app/overview/course-overview.component.ts | 16 +++++----- .../cit/aet/artemis/FaqIntegrationTest.java | 1 - 6 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java index bc025fb19adf..8690ae7678f1 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java @@ -5,6 +5,7 @@ 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; @Profile(PROFILE_CORE) @@ -27,4 +28,8 @@ public void deleteById(long faqId) { } + public Faq save(Faq faq) { + return faqRepository.save(faq); + } + } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java index 6b6bc74fe6b9..25c4d1d2cfdf 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java @@ -79,7 +79,7 @@ public ResponseEntity createFaq(@RequestBody Faq faq) throws URISyntaxExcep } authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); - Faq savedFaq = faqRepository.save(faq); + Faq savedFaq = faqService.save(faq); return ResponseEntity.created(new URI("/api/faqs/" + savedFaq.getId())).body(savedFaq); } @@ -92,13 +92,14 @@ public ResponseEntity createFaq(@RequestBody Faq faq) throws URISyntaxExcep */ @PutMapping("faqs/{faqId}") @EnforceAtLeastInstructor - public ResponseEntity updateFaq(@RequestBody Faq faq) { + public ResponseEntity updateFaq(@RequestBody Faq faq, @PathVariable String faqId) { log.debug("REST request to update Faq : {}", faq); - if (faq.getId() == null) { + if (faqId == null) { throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idNull"); } authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); - Faq result = faqRepository.save(faq); + Faq existingFaq = faqRepository.findById(faq.getId()).orElseThrow(() -> new BadRequestAlertException("FAQ not found", ENTITY_NAME, "idNotFound")); + Faq result = faqService.save(faq); return ResponseEntity.ok().body(result); } @@ -113,6 +114,7 @@ public ResponseEntity updateFaq(@RequestBody Faq faq) { public ResponseEntity getFaq(@PathVariable Long faqId) { log.debug("REST request to get faq {}", faqId); Faq faq = faqRepository.findById(faqId).orElseThrow(); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, faq.getCourse(), null); return ResponseEntity.ok(faq); } @@ -127,8 +129,9 @@ public ResponseEntity getFaq(@PathVariable Long faqId) { public ResponseEntity deleteFaq(@PathVariable Long faqId) { log.debug("REST request to delete faq {}", faqId); + Faq faq = faqRepository.findById(faqId).orElseThrow(); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, faq.getCourse(), null); faqService.deleteById(faqId); - return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, faqId.toString())).build(); } @@ -143,9 +146,7 @@ public ResponseEntity deleteFaq(@PathVariable Long faqId) { public ResponseEntity> 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); - + Course course = getCourseForRequest(courseId); Set faqs = faqRepository.findAllByCourseId(courseId); return ResponseEntity.ok().body(faqs); } @@ -161,12 +162,22 @@ public ResponseEntity> getFaqForCourse(@PathVariable Long courseId) { public ResponseEntity> 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); + Course course = getCourseForRequest(courseId); Set faqs = faqRepository.findAllCategoriesByCourseId(courseId); return ResponseEntity.ok().body(faqs); } + /** + * + * @param courseId the courseId of the course + * @return the course with the id courseId, unless it exists + */ + private Course getCourseForRequest(Long courseId) { + Course course = courseRepository.findByIdElseThrow(courseId); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); + return course; + } + } diff --git a/src/main/webapp/app/course/manage/course-update.component.ts b/src/main/webapp/app/course/manage/course-update.component.ts index 2931bb9cace0..87e053e301e3 100644 --- a/src/main/webapp/app/course/manage/course-update.component.ts +++ b/src/main/webapp/app/course/manage/course-update.component.ts @@ -71,7 +71,7 @@ export class CourseUpdateComponent implements OnInit { faExclamationTriangle = faExclamationTriangle; faPen = faPen; - faqEnabled = true; + faqEnabled: boolean; communicationEnabled = true; messagingEnabled = true; ltiEnabled = false; diff --git a/src/main/webapp/app/faq/faq-update.component.html b/src/main/webapp/app/faq/faq-update.component.html index 322bd3fe173e..f53c037fe670 100644 --- a/src/main/webapp/app/faq/faq-update.component.html +++ b/src/main/webapp/app/faq/faq-update.component.html @@ -15,9 +15,9 @@

-
+
- +
diff --git a/src/main/webapp/app/overview/course-overview.component.ts b/src/main/webapp/app/overview/course-overview.component.ts index 40693bd350ce..9ec72eec1fdb 100644 --- a/src/main/webapp/app/overview/course-overview.component.ts +++ b/src/main/webapp/app/overview/course-overview.component.ts @@ -335,11 +335,13 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit if (this.course?.faqEnabled) { const faqItem: SidebarItem = this.getFaqItem(); sidebarItems.push(faqItem); - if (this.course?.learningPathsEnabled) { - const learningPathItem: SidebarItem = this.getLearningPathItems(); - sidebarItems.push(learningPathItem); - } } + + if (this.course?.learningPathsEnabled) { + const learningPathItem: SidebarItem = this.getLearningPathItems(); + sidebarItems.push(learningPathItem); + } + return sidebarItems; } @@ -449,16 +451,16 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit } getFaqItem() { - const dashboardItem: SidebarItem = { + const faqItem: SidebarItem = { routerLink: 'faq', icon: faQuestion, - title: 'Faqs', + title: 'FAQs', translation: 'artemisApp.courseOverview.menu.faq', hasInOrionProperty: false, showInOrionWindow: false, hidden: false, }; - return dashboardItem; + return faqItem; } getDefaultItems() { diff --git a/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java index 61c373619fa7..c56f34ad916d 100644 --- a/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java @@ -45,7 +45,6 @@ void initTestCase() throws Exception { private void testAllPreAuthorize() throws Exception { request.postWithResponseBody("/api/faqs", new Faq(), Faq.class, HttpStatus.FORBIDDEN); request.putWithResponseBody("/api/faqs/" + this.faq.getId(), this.faq, Faq.class, HttpStatus.FORBIDDEN); - request.getList("/api/courses/" + course1.getId() + "/faqs", HttpStatus.OK, Faq.class); request.delete("/api/faqs/" + this.faq.getId(), HttpStatus.FORBIDDEN); } From ddca96ec9ac358d7eef4ae4aa82dcc7d81641599 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 18 Sep 2024 15:03:35 +0200 Subject: [PATCH 52/68] Fixed to enable faq for course --- .../webapp/app/course/manage/course-update.component.html | 4 ++-- src/main/webapp/app/course/manage/course-update.component.ts | 2 +- src/main/webapp/i18n/de/course.json | 2 +- src/main/webapp/i18n/en/course.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/app/course/manage/course-update.component.html b/src/main/webapp/app/course/manage/course-update.component.html index 7010ac06bd55..e48fd621970f 100644 --- a/src/main/webapp/app/course/manage/course-update.component.html +++ b/src/main/webapp/app/course/manage/course-update.component.html @@ -310,8 +310,8 @@
}
- - + + Date: Wed, 18 Sep 2024 15:42:26 +0200 Subject: [PATCH 53/68] Another coderabit hint --- .../cit/aet/artemis/communication/service/FaqService.java | 5 +++++ .../cit/aet/artemis/communication/web/FaqResource.java | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java index 8690ae7678f1..cf491d78c130 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java @@ -2,6 +2,8 @@ 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.stereotype.Service; @@ -32,4 +34,7 @@ public Faq save(Faq faq) { return faqRepository.save(faq); } + public Set findAllCategoriesByCourseId(Long courseId) { + faqRepository.findAllCategoriesByCourseId(courseId); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java index 25c4d1d2cfdf..a0826414ceed 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java @@ -92,9 +92,9 @@ public ResponseEntity createFaq(@RequestBody Faq faq) throws URISyntaxExcep */ @PutMapping("faqs/{faqId}") @EnforceAtLeastInstructor - public ResponseEntity updateFaq(@RequestBody Faq faq, @PathVariable String faqId) { + public ResponseEntity updateFaq(@RequestBody Faq faq, @PathVariable Long faqId) { log.debug("REST request to update Faq : {}", faq); - if (faqId == null) { + if (faqId == null || faqId.equals(faq.getId())) { throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idNull"); } authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); @@ -130,7 +130,7 @@ public ResponseEntity deleteFaq(@PathVariable Long faqId) { log.debug("REST request to delete faq {}", faqId); Faq faq = faqRepository.findById(faqId).orElseThrow(); - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, faq.getCourse(), null); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); faqService.deleteById(faqId); return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, faqId.toString())).build(); } @@ -164,7 +164,7 @@ public ResponseEntity> getFaqCategoriesForCourse(@PathVariable Long Course course = getCourseForRequest(courseId); - Set faqs = faqRepository.findAllCategoriesByCourseId(courseId); + Set faqs = faqService.findAllCategoriesByCourseId(courseId); return ResponseEntity.ok().body(faqs); } From d767dd985ac9863f5891f0bcc15698dc249f2022 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 18 Sep 2024 16:24:49 +0200 Subject: [PATCH 54/68] Another coderabit hint --- .../aet/artemis/communication/service/FaqService.java | 9 ++++++--- .../cit/aet/artemis/communication/web/FaqResource.java | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java index cf491d78c130..d1b3df4d2f5c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java @@ -10,6 +10,9 @@ 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 { @@ -23,7 +26,7 @@ public FaqService(FaqRepository faqRepository) { /** * Deletes the given faq * - * @param faqId the faqId of to be deleted faq + * @param faqId the ID of the FAQ to be deleted */ public void deleteById(long faqId) { faqRepository.deleteById(faqId); @@ -34,7 +37,7 @@ public Faq save(Faq faq) { return faqRepository.save(faq); } - public Set findAllCategoriesByCourseId(Long courseId) { - faqRepository.findAllCategoriesByCourseId(courseId); + public Set findAllCategoriesByCourseId(long courseId) { + return faqRepository.findAllCategoriesByCourseId(courseId); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java index a0826414ceed..c522abd8b3e9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java @@ -94,7 +94,7 @@ public ResponseEntity createFaq(@RequestBody Faq faq) throws URISyntaxExcep @EnforceAtLeastInstructor public ResponseEntity updateFaq(@RequestBody Faq faq, @PathVariable Long faqId) { log.debug("REST request to update Faq : {}", faq); - if (faqId == null || faqId.equals(faq.getId())) { + if (faqId == null || !faqId.equals(faq.getId())) { throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idNull"); } authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); @@ -113,7 +113,7 @@ public ResponseEntity updateFaq(@RequestBody Faq faq, @PathVariable Long fa @EnforceAtLeastStudent public ResponseEntity getFaq(@PathVariable Long faqId) { log.debug("REST request to get faq {}", faqId); - Faq faq = faqRepository.findById(faqId).orElseThrow(); + Faq faq = faqRepository.findById(faqId).orElseThrow(() -> new BadRequestAlertException("FAQ not found", ENTITY_NAME, "idNotFound")); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, faq.getCourse(), null); return ResponseEntity.ok(faq); } @@ -129,7 +129,7 @@ public ResponseEntity getFaq(@PathVariable Long faqId) { public ResponseEntity deleteFaq(@PathVariable Long faqId) { log.debug("REST request to delete faq {}", faqId); - Faq faq = faqRepository.findById(faqId).orElseThrow(); + Faq faq = faqRepository.findById(faqId).orElseThrow(() -> new BadRequestAlertException("FAQ not found", ENTITY_NAME, "idNotFound")); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); faqService.deleteById(faqId); return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, faqId.toString())).build(); From b5929e3868e71539f455db18ddd2d00525fb9680 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 18 Sep 2024 16:57:36 +0200 Subject: [PATCH 55/68] Remove repo from resource --- .../artemis/communication/service/FaqService.java | 9 +++++++++ .../aet/artemis/communication/web/FaqResource.java | 14 +++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java index d1b3df4d2f5c..127fe3af84f6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java @@ -2,6 +2,7 @@ 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; @@ -40,4 +41,12 @@ public Faq save(Faq faq) { public Set findAllCategoriesByCourseId(long courseId) { return faqRepository.findAllCategoriesByCourseId(courseId); } + + public Optional findById(Long faqId) { + return faqRepository.findById(faqId); + } + + public Set findAllByCourseId(Long courseId) { + return faqRepository.findAllByCourseId(courseId); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java index c522abd8b3e9..56c92fe29774 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java @@ -21,7 +21,6 @@ import org.springframework.web.bind.annotation.RestController; import de.tum.cit.aet.artemis.communication.domain.Faq; -import de.tum.cit.aet.artemis.communication.repository.FaqRepository; 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; @@ -47,17 +46,14 @@ public class FaqResource { @Value("${jhipster.clientApp.name}") private String applicationName; - private final FaqRepository faqRepository; - private final FaqService faqService; private final CourseRepository courseRepository; private final AuthorizationCheckService authCheckService; - public FaqResource(FaqRepository faqRepository, FaqService faqService, CourseRepository courseRepository, AuthorizationCheckService authCheckService) { + public FaqResource(FaqService faqService, CourseRepository courseRepository, AuthorizationCheckService authCheckService) { - this.faqRepository = faqRepository; this.faqService = faqService; this.courseRepository = courseRepository; this.authCheckService = authCheckService; @@ -98,7 +94,7 @@ public ResponseEntity updateFaq(@RequestBody Faq faq, @PathVariable Long fa throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idNull"); } authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); - Faq existingFaq = faqRepository.findById(faq.getId()).orElseThrow(() -> new BadRequestAlertException("FAQ not found", ENTITY_NAME, "idNotFound")); + Faq existingFaq = faqService.findById(faqId).orElseThrow(() -> new BadRequestAlertException("FAQ not found", ENTITY_NAME, "idNotFound")); Faq result = faqService.save(faq); return ResponseEntity.ok().body(result); } @@ -113,7 +109,7 @@ public ResponseEntity updateFaq(@RequestBody Faq faq, @PathVariable Long fa @EnforceAtLeastStudent public ResponseEntity getFaq(@PathVariable Long faqId) { log.debug("REST request to get faq {}", faqId); - Faq faq = faqRepository.findById(faqId).orElseThrow(() -> new BadRequestAlertException("FAQ not found", ENTITY_NAME, "idNotFound")); + Faq faq = faqService.findById(faqId).orElseThrow(() -> new BadRequestAlertException("FAQ not found", ENTITY_NAME, "idNotFound")); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, faq.getCourse(), null); return ResponseEntity.ok(faq); } @@ -129,7 +125,7 @@ public ResponseEntity getFaq(@PathVariable Long faqId) { public ResponseEntity deleteFaq(@PathVariable Long faqId) { log.debug("REST request to delete faq {}", faqId); - Faq faq = faqRepository.findById(faqId).orElseThrow(() -> new BadRequestAlertException("FAQ not found", ENTITY_NAME, "idNotFound")); + Faq faq = faqService.findById(faqId).orElseThrow(() -> new BadRequestAlertException("FAQ not found", ENTITY_NAME, "idNotFound")); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); faqService.deleteById(faqId); return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, faqId.toString())).build(); @@ -147,7 +143,7 @@ public ResponseEntity> getFaqForCourse(@PathVariable Long courseId) { log.debug("REST request to get all Faqs for the course with id : {}", courseId); Course course = getCourseForRequest(courseId); - Set faqs = faqRepository.findAllByCourseId(courseId); + Set faqs = faqService.findAllByCourseId(courseId); return ResponseEntity.ok().body(faqs); } From c0e1432d7afcb044103851b56685674c69442ec7 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 19 Sep 2024 09:56:06 +0200 Subject: [PATCH 56/68] Remove repo from resource --- .../artemis/communication/web/FaqResource.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java index 56c92fe29774..d828bf551f24 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java @@ -24,6 +24,7 @@ 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; @@ -91,10 +92,10 @@ public ResponseEntity createFaq(@RequestBody Faq faq) throws URISyntaxExcep public ResponseEntity 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("Invalid id", ENTITY_NAME, "idNull"); + throw new BadRequestAlertException("Id of FAQ and path must match", ENTITY_NAME, "idNull"); } authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); - Faq existingFaq = faqService.findById(faqId).orElseThrow(() -> new BadRequestAlertException("FAQ not found", ENTITY_NAME, "idNotFound")); + Faq existingFaq = faqService.findById(faqId).orElseThrow(() -> new EntityNotFoundException("FAQ not found", faqId)); Faq result = faqService.save(faq); return ResponseEntity.ok().body(result); } @@ -109,7 +110,7 @@ public ResponseEntity updateFaq(@RequestBody Faq faq, @PathVariable Long fa @EnforceAtLeastStudent public ResponseEntity getFaq(@PathVariable Long faqId) { log.debug("REST request to get faq {}", faqId); - Faq faq = faqService.findById(faqId).orElseThrow(() -> new BadRequestAlertException("FAQ not found", ENTITY_NAME, "idNotFound")); + Faq faq = faqService.findById(faqId).orElseThrow(() -> new EntityNotFoundException("FAQ not found", faqId)); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, faq.getCourse(), null); return ResponseEntity.ok(faq); } @@ -125,7 +126,7 @@ public ResponseEntity getFaq(@PathVariable Long faqId) { public ResponseEntity deleteFaq(@PathVariable Long faqId) { log.debug("REST request to delete faq {}", faqId); - Faq faq = faqService.findById(faqId).orElseThrow(() -> new BadRequestAlertException("FAQ not found", ENTITY_NAME, "idNotFound")); + 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(); @@ -143,6 +144,7 @@ public ResponseEntity> 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 faqs = faqService.findAllByCourseId(courseId); return ResponseEntity.ok().body(faqs); } @@ -159,7 +161,7 @@ public ResponseEntity> getFaqCategoriesForCourse(@PathVariable Long 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 faqs = faqService.findAllCategoriesByCourseId(courseId); return ResponseEntity.ok().body(faqs); @@ -171,9 +173,7 @@ public ResponseEntity> getFaqCategoriesForCourse(@PathVariable Long * @return the course with the id courseId, unless it exists */ private Course getCourseForRequest(Long courseId) { - Course course = courseRepository.findByIdElseThrow(courseId); - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); - return course; + return courseRepository.findByIdElseThrow(courseId); } } From bd8a8c868f1c4bfeedb70ee79ed812d7f7d0a00f Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 19 Sep 2024 11:35:05 +0200 Subject: [PATCH 57/68] Added client test for faq.service --- src/main/webapp/app/faq/faq.service.ts | 27 +--- .../spec/service/faq.service.spec.ts | 139 ++++++++++++++++++ 2 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 src/test/javascript/spec/service/faq.service.spec.ts diff --git a/src/main/webapp/app/faq/faq.service.ts b/src/main/webapp/app/faq/faq.service.ts index 5a05648723d0..1430b2ec8dc0 100644 --- a/src/main/webapp/app/faq/faq.service.ts +++ b/src/main/webapp/app/faq/faq.service.ts @@ -3,10 +3,8 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { Faq, FaqState } from 'app/entities/faq.model'; -import { Exercise } from 'app/entities/exercise.model'; import { FaqCategory } from 'app/entities/faq-category.model'; import { AlertService } from 'app/core/util/alert.service'; -import { ExerciseCategory } from 'app/entities/exercise-category.model'; type EntityResponseType = HttpResponse; type EntityArrayResponseType = HttpResponse; @@ -48,7 +46,7 @@ export class FaqService { .get(this.resourceUrl + `/${courseId}/faqs`, { observe: 'response', }) - .pipe(map((res: EntityArrayResponseType) => FaqService.convertExerciseCategoryArrayFromServer(res))); + .pipe(map((res: EntityArrayResponseType) => FaqService.convertFaqCategoryArrayFromServer(res))); } delete(faqId: number): Observable> { @@ -66,7 +64,7 @@ export class FaqService { */ static convertFaqCategoriesFromServer(res: ERT): ERT { if (res.body && res.body.categories) { - FaqService.parseExerciseCategories(res.body); + FaqService.parseFaqCategories(res.body); } return res; } @@ -79,7 +77,7 @@ export class FaqService { return faq.categories?.map((category) => JSON.stringify(category) as unknown as FaqCategory); } - convertFaqCategoriesAsStringFromServer(categories: string[]): ExerciseCategory[] { + convertFaqCategoriesAsStringFromServer(categories: string[]): FaqCategory[] { return categories.map((category) => JSON.parse(category)); } @@ -87,18 +85,18 @@ export class FaqService { * Converts the faq category json strings into FaqCategory objects (if it exists). * @param res the response */ - static convertExerciseCategoryArrayFromServer(res: EART): EART { + static convertFaqCategoryArrayFromServer(res: EART): EART { if (res.body) { - res.body.forEach((exercise: E) => FaqService.parseExerciseCategories(exercise)); + res.body.forEach((faq: E) => FaqService.parseFaqCategories(faq)); } return res; } /** * Parses the faq categories JSON string into {@link FaqCategory} objects. - * @param faq - the exercise + * @param faq - the faq */ - static parseExerciseCategories(faq?: Faq) { + static parseFaqCategories(faq?: Faq) { if (faq?.categories) { faq.categories = faq.categories.map((category) => { const categoryObj = JSON.parse(category as unknown as string); @@ -107,17 +105,6 @@ export class FaqService { } } - static parseFaqCategoriesString(categories?: string[]) { - let faqCategories: FaqCategory[] = []; - if (categories) { - faqCategories = categories.map((category) => { - const categoryObj = JSON.parse(category as unknown as string); - return new FaqCategory(categoryObj.category, categoryObj.color); - }); - } - return faqCategories; - } - /** * Prepare client-faq to be uploaded to the server * @param { Faq } faq - faq that will be modified diff --git a/src/test/javascript/spec/service/faq.service.spec.ts b/src/test/javascript/spec/service/faq.service.spec.ts new file mode 100644 index 000000000000..20ad9e87e448 --- /dev/null +++ b/src/test/javascript/spec/service/faq.service.spec.ts @@ -0,0 +1,139 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { HttpResponse } from '@angular/common/http'; +import { take } from 'rxjs/operators'; +import { ArtemisTestModule } from '../test.module'; +import { LocalStorageService, SessionStorageService } from 'ngx-webstorage'; +import { MockSyncStorage } from '../helpers/mocks/service/mock-sync-storage.service'; +import { TranslateService } from '@ngx-translate/core'; +import { MockTranslateService } from '../helpers/mocks/service/mock-translate.service'; +import { Course } from 'app/entities/course.model'; +import { Faq, FaqState } from 'app/entities/faq.model'; +import { FaqCategory } from 'app/entities/faq-category.model'; +import { FaqService } from 'app/faq/faq.service'; + +describe('Faq Service', () => { + let httpMock: HttpTestingController; + let service: FaqService; + const resourceUrl = 'api/faqs'; + let expectedResult: any; + let elemDefault: Faq; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, HttpClientTestingModule], + providers: [ + { provide: LocalStorageService, useClass: MockSyncStorage }, + { provide: SessionStorageService, useClass: MockSyncStorage }, + { provide: TranslateService, useClass: MockTranslateService }, + ], + }); + service = TestBed.inject(FaqService); + httpMock = TestBed.inject(HttpTestingController); + + expectedResult = {} as HttpResponse; + elemDefault = new Faq(); + elemDefault.questionTitle = 'Title'; + elemDefault.course = new Course(); + elemDefault.questionAnswer = 'Answer'; + elemDefault.id = 1; + elemDefault.faqState = FaqState.ACCEPTED; + }); + + afterEach(() => { + httpMock.verify(); + }); + + describe('Service methods', () => { + it('should create a faq in the database', async () => { + const returnedFromService = { ...elemDefault }; + const expected = { ...returnedFromService }; + service + .create(elemDefault) + .pipe(take(1)) + .subscribe((resp) => (expectedResult = resp)); + const req = httpMock.expectOne({ + url: resourceUrl, + method: 'POST', + }); + req.flush(returnedFromService); + expect(expectedResult.body).toEqual(expected); + }); + + it('should update a faq in the database', async () => { + const returnedFromService = { ...elemDefault }; + const expected = { ...returnedFromService }; + const faqId = elemDefault.id!; + service + .update(elemDefault) + .pipe(take(1)) + .subscribe((resp) => (expectedResult = resp)); + const req = httpMock.expectOne({ + url: `${resourceUrl}/${faqId}`, + method: 'PUT', + }); + req.flush(returnedFromService); + expect(expectedResult.body).toEqual(expected); + }); + + it('should find a faq in the database', async () => { + const category = { + color: '#6ae8ac', + category: 'category1', + } as FaqCategory; + const returnedFromService = { ...elemDefault, categories: [JSON.stringify(category)] }; + const expected = { ...elemDefault, categories: [new FaqCategory('category1', '#6ae8ac')] }; + const faqId = elemDefault.id!; + service + .find(faqId) + .pipe(take(1)) + .subscribe((resp) => (expectedResult = resp)); + const req = httpMock.expectOne({ + url: `${resourceUrl}/${faqId}`, + method: 'GET', + }); + req.flush(returnedFromService); + expect(expectedResult.body).toEqual(expected); + }); + + it('should find faqs by courseId in the database', async () => { + const category = { + color: '#6ae8ac', + category: 'category1', + } as FaqCategory; + const returnedFromService = [{ ...elemDefault, categories: [JSON.stringify(category)] }]; + const expected = [{ ...elemDefault, categories: [new FaqCategory('category1', '#6ae8ac')] }]; + const courseId = 1; + service + .findAllByCourseId(courseId) + .pipe(take(1)) + .subscribe((resp) => (expectedResult = resp)); + const req = httpMock.expectOne({ + url: `api/courses/${courseId}/faqs`, + method: 'GET', + }); + req.flush(returnedFromService); + expect(expectedResult.body).toEqual(expected); + }); + + it('should find all categories by courseId in the database', async () => { + const category = { + color: '#6ae8ac', + category: 'category1', + } as FaqCategory; + const returnedFromService = { categories: [JSON.stringify(category)] }; + const expected = { ...returnedFromService }; + const courseId = 1; + service + .findAllCategoriesByCourseId(courseId) + .pipe(take(1)) + .subscribe((resp) => (expectedResult = resp)); + const req = httpMock.expectOne({ + url: `api/courses/${courseId}/faq-categories`, + method: 'GET', + }); + req.flush(returnedFromService); + expect(expectedResult.body).toEqual(expected); + }); + }); +}); From cc9c3e54a894cb8bdb56c9b98cc2e1a03986ee44 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Fri, 20 Sep 2024 10:36:00 +0200 Subject: [PATCH 58/68] Adjusted naming to be equal --- .../webapp/app/entities/faq-category.model.ts | 6 ++-- src/main/webapp/app/entities/faq.model.ts | 4 +-- .../webapp/app/faq/faq-update.component.ts | 12 ++++---- src/main/webapp/app/faq/faq.component.html | 2 +- src/main/webapp/app/faq/faq.component.ts | 16 +++++----- src/main/webapp/app/faq/faq.routes.ts | 4 +-- src/main/webapp/app/faq/faq.service.ts | 30 +++++++++---------- src/main/webapp/app/faq/faq.utils.ts | 6 ++-- .../course-faq/course-faq.component.ts | 12 ++++---- .../category-selector.component.ts | 6 ++-- ...ustom-exercise-category-badge.component.ts | 4 +-- 11 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/main/webapp/app/entities/faq-category.model.ts b/src/main/webapp/app/entities/faq-category.model.ts index 6d62502ac923..b7ef47b24d13 100644 --- a/src/main/webapp/app/entities/faq-category.model.ts +++ b/src/main/webapp/app/entities/faq-category.model.ts @@ -1,4 +1,4 @@ -export class FaqCategory { +export class FAQCategory { public color?: string; public category?: string; @@ -8,7 +8,7 @@ export class FaqCategory { this.category = category; } - equals(otherExerciseCategory: FaqCategory): boolean { + equals(otherExerciseCategory: FAQCategory): boolean { return this.color === otherExerciseCategory.color && this.category === otherExerciseCategory.category; } @@ -16,7 +16,7 @@ export class FaqCategory { * @param otherExerciseCategory * @returns the alphanumerical order of the two exercise categories based on their display text */ - compare(otherExerciseCategory: FaqCategory): number { + compare(otherExerciseCategory: FAQCategory): number { if (this.category === otherExerciseCategory.category) { return 0; } diff --git a/src/main/webapp/app/entities/faq.model.ts b/src/main/webapp/app/entities/faq.model.ts index f71748c89c66..ab9cadc0664b 100644 --- a/src/main/webapp/app/entities/faq.model.ts +++ b/src/main/webapp/app/entities/faq.model.ts @@ -1,6 +1,6 @@ import { BaseEntity } from 'app/shared/model/base-entity'; import { Course } from 'app/entities/course.model'; -import { FaqCategory } from './faq-category.model'; +import { FAQCategory } from './faq-category.model'; export enum FaqState { ACCEPTED, @@ -14,7 +14,7 @@ export class Faq implements BaseEntity { public questionAnswer?: string; public faqState?: FaqState; public course?: Course; - public categories?: FaqCategory[]; + public categories?: FAQCategory[]; constructor() {} } diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index 3b3b5d2c61c2..a7bfb4a3f4b0 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -10,9 +10,9 @@ import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { faBan, faQuestionCircle, faSave } from '@fortawesome/free-solid-svg-icons'; import { FormulaAction } from 'app/shared/monaco-editor/model/actions/formula.action'; import { Faq } from 'app/entities/faq.model'; -import { FaqService } from 'app/faq/faq.service'; +import { FAQService } from 'app/faq/faq.service'; import { TranslateService } from '@ngx-translate/core'; -import { FaqCategory } from 'app/entities/faq-category.model'; +import { FAQCategory } from 'app/entities/faq-category.model'; import { loadCourseFaqCategories } from 'app/faq/faq.utils'; import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; import { ArtemisCategorySelectorModule } from 'app/shared/category-selector/category-selector.module'; @@ -29,8 +29,8 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo export class FAQUpdateComponent implements OnInit { faq: Faq; isSaving: boolean; - existingCategories: FaqCategory[] = []; - faqCategories: FaqCategory[] = []; + existingCategories: FAQCategory[] = []; + faqCategories: FAQCategory[] = []; courses: Course[]; @@ -45,7 +45,7 @@ export class FAQUpdateComponent implements OnInit { constructor( protected alertService: AlertService, - protected faqService: FaqService, + protected faqService: FAQService, protected courseService: CourseManagementService, protected activatedRoute: ActivatedRoute, private navigationUtilService: ArtemisNavigationUtilService, @@ -142,7 +142,7 @@ export class FAQUpdateComponent implements OnInit { } } - updateCategories(categories: FaqCategory[]) { + updateCategories(categories: FAQCategory[]) { this.faq.categories = categories; this.faqCategories = categories; } diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index 8783e54e9aa0..e90d5d8023ac 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -69,7 +69,7 @@

{{ faq.id }} diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index 0c86861bfd4b..918aa7a571a2 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -5,10 +5,10 @@ import { Subject } from 'rxjs'; import { map } from 'rxjs/operators'; import { AlertService } from 'app/core/util/alert.service'; import { ActivatedRoute } from '@angular/router'; -import { FaqService } from 'app/faq/faq.service'; +import { FAQService } from 'app/faq/faq.service'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { onError } from 'app/shared/util/global.utils'; -import { FaqCategory } from 'app/entities/faq-category.model'; +import { FAQCategory } from 'app/entities/faq-category.model'; import { loadCourseFaqCategories } from 'app/faq/faq.utils'; import { SortService } from 'app/shared/service/sort.service'; import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; @@ -25,8 +25,8 @@ import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; }) export class FAQComponent implements OnInit, OnDestroy { faqs: Faq[]; - filteredFaq: Faq[]; - existingCategories: FaqCategory[]; + filteredFaqs: Faq[]; + existingCategories: FAQCategory[]; courseId: number; private dialogErrorSource = new Subject(); @@ -51,7 +51,7 @@ export class FAQComponent implements OnInit, OnDestroy { faSort = faSort; constructor( - protected faqService: FaqService, + protected faqService: FAQService, private route: ActivatedRoute, private alertService: AlertService, private sortService: SortService, @@ -88,16 +88,16 @@ export class FAQComponent implements OnInit, OnDestroy { } toggleFilters(category: string) { - this.activeFilters = FaqService.toggleFilter(category, this.activeFilters); + this.activeFilters = FAQService.toggleFilter(category, this.activeFilters); this.applyFilters(); } private applyFilters(): void { - this.filteredFaq = FaqService.applyFilters(this.activeFilters, this.faqs); + this.filteredFaqs = FAQService.applyFilters(this.activeFilters, this.faqs); } sortRows() { - this.sortService.sortByProperty(this.filteredFaq, this.predicate, this.ascending); + this.sortService.sortByProperty(this.filteredFaqs, this.predicate, this.ascending); } private loadAll() { diff --git a/src/main/webapp/app/faq/faq.routes.ts b/src/main/webapp/app/faq/faq.routes.ts index 022c836d157c..fc5859fc7659 100644 --- a/src/main/webapp/app/faq/faq.routes.ts +++ b/src/main/webapp/app/faq/faq.routes.ts @@ -8,13 +8,13 @@ import { Authority } from 'app/shared/constants/authority.constants'; import { CourseManagementResolve } from 'app/course/manage/course-management-resolve.service'; import { CourseManagementTabBarComponent } from 'app/course/manage/course-management-tab-bar/course-management-tab-bar.component'; import { FAQComponent } from 'app/faq/faq.component'; -import { FaqService } from 'app/faq/faq.service'; +import { FAQService } from 'app/faq/faq.service'; import { Faq } from 'app/entities/faq.model'; import { FAQUpdateComponent } from 'app/faq/faq-update.component'; @Injectable({ providedIn: 'root' }) export class FAQResolve implements Resolve { - constructor(private faqService: FaqService) {} + constructor(private faqService: FAQService) {} resolve(route: ActivatedRouteSnapshot): Observable { const faqId = route.params['faqId']; diff --git a/src/main/webapp/app/faq/faq.service.ts b/src/main/webapp/app/faq/faq.service.ts index 1430b2ec8dc0..e7384713623c 100644 --- a/src/main/webapp/app/faq/faq.service.ts +++ b/src/main/webapp/app/faq/faq.service.ts @@ -3,14 +3,14 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { Faq, FaqState } from 'app/entities/faq.model'; -import { FaqCategory } from 'app/entities/faq-category.model'; +import { FAQCategory } from 'app/entities/faq-category.model'; import { AlertService } from 'app/core/util/alert.service'; type EntityResponseType = HttpResponse; type EntityArrayResponseType = HttpResponse; @Injectable({ providedIn: 'root' }) -export class FaqService { +export class FAQService { public resourceUrl = 'api/courses'; constructor( @@ -19,7 +19,7 @@ export class FaqService { ) {} create(faq: Faq): Observable { - const copy = FaqService.convertFaqFromClient(faq); + const copy = FAQService.convertFaqFromClient(faq); faq.faqState = FaqState.ACCEPTED; return this.http.post(`api/faqs`, copy, { observe: 'response' }).pipe( map((res: EntityResponseType) => { @@ -29,7 +29,7 @@ export class FaqService { } update(faq: Faq): Observable { - const copy = FaqService.convertFaqFromClient(faq); + const copy = FAQService.convertFaqFromClient(faq); return this.http.put(`api/faqs/${faq.id}`, copy, { observe: 'response' }).pipe( map((res: EntityResponseType) => { return res; @@ -38,7 +38,7 @@ export class FaqService { } find(faqId: number): Observable { - return this.http.get(`api/faqs/${faqId}`, { observe: 'response' }).pipe(map((res: EntityResponseType) => FaqService.convertFaqCategoriesFromServer(res))); + return this.http.get(`api/faqs/${faqId}`, { observe: 'response' }).pipe(map((res: EntityResponseType) => FAQService.convertFaqCategoriesFromServer(res))); } findAllByCourseId(courseId: number): Observable { @@ -46,11 +46,11 @@ export class FaqService { .get(this.resourceUrl + `/${courseId}/faqs`, { observe: 'response', }) - .pipe(map((res: EntityArrayResponseType) => FaqService.convertFaqCategoryArrayFromServer(res))); + .pipe(map((res: EntityArrayResponseType) => FAQService.convertFaqCategoryArrayFromServer(res))); } - delete(faqId: number): Observable> { - return this.http.delete(`api/faqs/${faqId}`, { observe: 'response' }); + delete(faqId: number): Observable> { + return this.http.delete(`api/faqs/${faqId}`, { observe: 'response' }); } findAllCategoriesByCourseId(courseId: number) { @@ -64,7 +64,7 @@ export class FaqService { */ static convertFaqCategoriesFromServer(res: ERT): ERT { if (res.body && res.body.categories) { - FaqService.parseFaqCategories(res.body); + FAQService.parseFaqCategories(res.body); } return res; } @@ -74,10 +74,10 @@ export class FaqService { * @param faq the faq */ static stringifyFaqCategories(faq: Faq) { - return faq.categories?.map((category) => JSON.stringify(category) as unknown as FaqCategory); + return faq.categories?.map((category) => JSON.stringify(category) as unknown as FAQCategory); } - convertFaqCategoriesAsStringFromServer(categories: string[]): FaqCategory[] { + convertFaqCategoriesAsStringFromServer(categories: string[]): FAQCategory[] { return categories.map((category) => JSON.parse(category)); } @@ -87,20 +87,20 @@ export class FaqService { */ static convertFaqCategoryArrayFromServer(res: EART): EART { if (res.body) { - res.body.forEach((faq: E) => FaqService.parseFaqCategories(faq)); + res.body.forEach((faq: E) => FAQService.parseFaqCategories(faq)); } return res; } /** - * Parses the faq categories JSON string into {@link FaqCategory} objects. + * Parses the faq categories JSON string into {@link FAQCategory} objects. * @param faq - the faq */ static parseFaqCategories(faq?: Faq) { if (faq?.categories) { faq.categories = faq.categories.map((category) => { const categoryObj = JSON.parse(category as unknown as string); - return new FaqCategory(categoryObj.category, categoryObj.color); + return new FAQCategory(categoryObj.category, categoryObj.color); }); } } @@ -111,7 +111,7 @@ export class FaqService { */ static convertFaqFromClient(faq: F): Faq { const copy = Object.assign(faq, {}); - copy.categories = FaqService.stringifyFaqCategories(copy); + copy.categories = FAQService.stringifyFaqCategories(copy); if (copy.categories) { } return copy; diff --git a/src/main/webapp/app/faq/faq.utils.ts b/src/main/webapp/app/faq/faq.utils.ts index f96bffdbb575..6c083ad5dcde 100644 --- a/src/main/webapp/app/faq/faq.utils.ts +++ b/src/main/webapp/app/faq/faq.utils.ts @@ -2,10 +2,10 @@ import { onError } from 'app/shared/util/global.utils'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; import { Observable, catchError, map, of } from 'rxjs'; -import { FaqService } from 'app/faq/faq.service'; -import { FaqCategory } from 'app/entities/faq-category.model'; +import { FAQService } from 'app/faq/faq.service'; +import { FAQCategory } from 'app/entities/faq-category.model'; -export function loadCourseFaqCategories(courseId: number | undefined, alertService: AlertService, faqService: FaqService): Observable { +export function loadCourseFaqCategories(courseId: number | undefined, alertService: AlertService, faqService: FAQService): Observable { if (courseId === undefined) { return of([]); } diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts index 2bad9daf064c..4354af225fca 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -10,10 +10,10 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo import { ArtemisSharedModule } from 'app/shared/shared.module'; import { CourseFaqAccordionComponent } from 'app/overview/course-faq/course-faq-accordion-component'; import { Faq } from 'app/entities/faq.model'; -import { FaqService } from 'app/faq/faq.service'; +import { FAQService } from 'app/faq/faq.service'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; -import { FaqCategory } from 'app/entities/faq-category.model'; +import { FAQCategory } from 'app/entities/faq-category.model'; import { loadCourseFaqCategories } from 'app/faq/faq.utils'; import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; import { onError } from 'app/shared/util/global.utils'; @@ -35,7 +35,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy { faqs: Faq[]; filteredFaq: Faq[]; - existingCategories: FaqCategory[]; + existingCategories: FAQCategory[]; activeFilters = new Set(); sidebarData: SidebarData; @@ -53,7 +53,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy { constructor( private route: ActivatedRoute, private router: Router, - private faqService: FaqService, + private faqService: FAQService, private alertService: AlertService, ) {} @@ -91,11 +91,11 @@ export class CourseFaqComponent implements OnInit, OnDestroy { } toggleFilters(category: string) { - this.activeFilters = FaqService.toggleFilter(category, this.activeFilters); + this.activeFilters = FAQService.toggleFilter(category, this.activeFilters); this.applyFilters(); } private applyFilters(): void { - this.filteredFaq = FaqService.applyFilters(this.activeFilters, this.faqs); + this.filteredFaq = FAQService.applyFilters(this.activeFilters, this.faqs); } } diff --git a/src/main/webapp/app/shared/category-selector/category-selector.component.ts b/src/main/webapp/app/shared/category-selector/category-selector.component.ts index 4214f340ffca..983dcc52bd9b 100644 --- a/src/main/webapp/app/shared/category-selector/category-selector.component.ts +++ b/src/main/webapp/app/shared/category-selector/category-selector.component.ts @@ -7,7 +7,7 @@ import { FormControl } from '@angular/forms'; import { MatChipInputEvent } from '@angular/material/chips'; import { Observable, map, startWith } from 'rxjs'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; -import { FaqCategory } from 'app/entities/faq-category.model'; +import { FAQCategory } from 'app/entities/faq-category.model'; const DEFAULT_COLORS = ['#6ae8ac', '#9dca53', '#94a11c', '#691b0b', '#ad5658', '#1b97ca', '#0d3cc2', '#0ab84f']; @@ -23,12 +23,12 @@ export class CategorySelectorComponent implements OnChanges { /** * the selected categories, which can be manipulated by the user in the UI */ - @Input() categories: ExerciseCategory[] | FaqCategory[]; + @Input() categories: ExerciseCategory[] | FAQCategory[]; /** * the existing categories used for auto-completion, might include duplicates */ - @Input() existingCategories: ExerciseCategory[] | FaqCategory[]; + @Input() existingCategories: ExerciseCategory[] | FAQCategory[]; @Output() selectedCategories = new EventEmitter(); diff --git a/src/main/webapp/app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component.ts b/src/main/webapp/app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component.ts index aec203a26946..65640ff2925e 100644 --- a/src/main/webapp/app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component.ts +++ b/src/main/webapp/app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component.ts @@ -3,7 +3,7 @@ import type { ExerciseCategory } from 'app/entities/exercise-category.model'; import { CommonModule } from '@angular/common'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; -import { FaqCategory } from 'app/entities/faq-category.model'; +import { FAQCategory } from 'app/entities/faq-category.model'; type CategoryFontSize = 'default' | 'small'; @@ -17,7 +17,7 @@ type CategoryFontSize = 'default' | 'small'; export class CustomExerciseCategoryBadgeComponent { protected readonly faTimes = faTimes; - @Input({ required: true }) category: ExerciseCategory | FaqCategory; + @Input({ required: true }) category: ExerciseCategory | FAQCategory; @Input() displayRemoveButton: boolean = false; @Input() onClick: () => void = () => {}; @Input() fontSize: CategoryFontSize = 'default'; From 722366e7e7d1a9ee8f38107a076f6272110ea231 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Fri, 20 Sep 2024 10:57:35 +0200 Subject: [PATCH 59/68] Added first client tests for the components --- .../webapp/app/faq/faq-update.component.ts | 5 - src/main/webapp/app/faq/faq.component.html | 2 +- src/main/webapp/app/faq/faq.routes.ts | 2 +- .../faq/faq-update.component.spec.ts | 141 ++++++++++++++++++ .../spec/component/faq/faq.component.spec.ts | 129 ++++++++++++++++ .../spec/service/faq.service.spec.ts | 18 +-- 6 files changed, 281 insertions(+), 16 deletions(-) create mode 100644 src/test/javascript/spec/component/faq/faq-update.component.spec.ts create mode 100644 src/test/javascript/spec/component/faq/faq.component.spec.ts diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index a7bfb4a3f4b0..1db317441090 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -4,7 +4,6 @@ import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { AlertService } from 'app/core/util/alert.service'; import { CourseManagementService } from '../course/manage/course-management.service'; -import { Course } from 'app/entities/course.model'; import { onError } from 'app/shared/util/global.utils'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { faBan, faQuestionCircle, faSave } from '@fortawesome/free-solid-svg-icons'; @@ -32,11 +31,7 @@ export class FAQUpdateComponent implements OnInit { existingCategories: FAQCategory[] = []; faqCategories: FAQCategory[] = []; - courses: Course[]; - domainActionsDescription = [new FormulaAction()]; - file: File; - fileName: string; // Icons faQuestionCircle = faQuestionCircle; diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index e90d5d8023ac..eceb15b6b8af 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -34,7 +34,7 @@

} -
+
diff --git a/src/main/webapp/app/faq/faq.routes.ts b/src/main/webapp/app/faq/faq.routes.ts index fc5859fc7659..312319c60247 100644 --- a/src/main/webapp/app/faq/faq.routes.ts +++ b/src/main/webapp/app/faq/faq.routes.ts @@ -41,7 +41,7 @@ export const faqRoutes: Routes = [ }, data: { authorities: [Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], - pageTitle: '', + pageTitle: 'artemisApp.faq.home.title', }, canActivate: [UserRouteAccessService], }, diff --git a/src/test/javascript/spec/component/faq/faq-update.component.spec.ts b/src/test/javascript/spec/component/faq/faq-update.component.spec.ts new file mode 100644 index 000000000000..e99c4ebd2244 --- /dev/null +++ b/src/test/javascript/spec/component/faq/faq-update.component.spec.ts @@ -0,0 +1,141 @@ +import { HttpResponse } from '@angular/common/http'; +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { TranslateService } from '@ngx-translate/core'; +import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe'; +import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks'; +import { of } from 'rxjs'; +import { MockRouterLinkDirective } from '../../helpers/mocks/directive/mock-router-link.directive'; +import { MockRouter } from '../../helpers/mocks/mock-router'; +import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; +import { ArtemisTestModule } from '../../test.module'; +import { FAQUpdateComponent } from 'app/faq/faq-update.component'; +import { FAQService } from 'app/faq/faq.service'; +import { Faq } from 'app/entities/faq.model'; +import { MonacoEditorComponent } from 'app/shared/monaco-editor/monaco-editor.component'; +import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.module'; +import { MockResizeObserver } from '../../helpers/mocks/service/mock-resize-observer'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { AlertService } from 'app/core/util/alert.service'; + +describe('FaqUpdateComponent', () => { + let faqUpdateComponentFixture: ComponentFixture; + let faqUpdateComponent: FAQUpdateComponent; + let faqService: FAQService; + let activatedRoute: ActivatedRoute; + let router: Router; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, MonacoEditorModule, MockModule(BrowserAnimationsModule)], + declarations: [FAQUpdateComponent, MockComponent(MonacoEditorComponent), MockPipe(HtmlForMarkdownPipe), MockRouterLinkDirective], + providers: [ + { provide: TranslateService, useClass: MockTranslateService }, + { provide: Router, useClass: MockRouter }, + { + provide: ActivatedRoute, + useValue: { + parent: { + data: of({ course: { id: 1 } }), + }, + queryParams: of({ + params: {}, + }), + snapshot: { + paramMap: convertToParamMap({ + courseId: '1', + }), + }, + }, + }, + MockProvider(AlertService), + ], + }).compileComponents(); + + global.ResizeObserver = jest.fn().mockImplementation((callback: ResizeObserverCallback) => { + return new MockResizeObserver(callback); + }); + faqUpdateComponentFixture = TestBed.createComponent(FAQUpdateComponent); + faqUpdateComponent = faqUpdateComponentFixture.componentInstance; + + faqService = TestBed.inject(FAQService); + + router = TestBed.inject(Router); + activatedRoute = TestBed.inject(ActivatedRoute); + faqUpdateComponentFixture.detectChanges(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should create faq', fakeAsync(() => { + faqUpdateComponent.faq = { questionTitle: 'test1' } as Faq; + + const createSpy = jest.spyOn(faqService, 'create').mockReturnValue( + of( + new HttpResponse({ + body: { + id: 3, + questionTitle: 'test1', + course: { + id: 1, + }, + } as Faq, + }), + ), + ); + + faqUpdateComponent.save(); + tick(); + faqUpdateComponentFixture.detectChanges(); + + expect(createSpy).toHaveBeenCalledOnce(); + expect(createSpy).toHaveBeenCalledWith({ questionTitle: 'test1' }); + })); + + it('should edit a faq', fakeAsync(() => { + activatedRoute.parent!.data = of({ course: { id: 1 }, faq: { id: 6 } }); + + faqUpdateComponentFixture.detectChanges(); + faqUpdateComponent.faq = { id: 6, questionTitle: 'test1Updated' } as Faq; + + const updateSpy = jest.spyOn(faqService, 'update').mockReturnValue( + of>( + new HttpResponse({ + body: { + id: 6, + title: 'test1Updated', + course: { + id: 1, + }, + } as Faq, + }), + ), + ); + + faqUpdateComponent.save(); + tick(); + faqUpdateComponentFixture.detectChanges(); + + expect(updateSpy).toHaveBeenCalledOnce(); + expect(updateSpy).toHaveBeenCalledWith({ id: 6, questionTitle: 'test1Updated' }); + })); + + it('should navigate to previous state', fakeAsync(() => { + activatedRoute = TestBed.inject(ActivatedRoute); + activatedRoute.parent!.data = of({ course: { id: 1 }, faq: { id: 6, questionTitle: '', course: { id: 1 } } }); + + faqUpdateComponent.ngOnInit(); + faqUpdateComponentFixture.detectChanges(); + + const navigateSpy = jest.spyOn(router, 'navigate'); + const previousState = jest.spyOn(faqUpdateComponent, 'previousState'); + faqUpdateComponent.previousState(); + tick(); + expect(previousState).toHaveBeenCalledOnce(); + + const expectedPath = ['course-management', '1', 'faqs']; + expect(navigateSpy).toHaveBeenCalledWith(expectedPath); + })); +}); diff --git a/src/test/javascript/spec/component/faq/faq.component.spec.ts b/src/test/javascript/spec/component/faq/faq.component.spec.ts new file mode 100644 index 000000000000..1b6a46238f32 --- /dev/null +++ b/src/test/javascript/spec/component/faq/faq.component.spec.ts @@ -0,0 +1,129 @@ +import { HttpResponse } from '@angular/common/http'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateService } from '@ngx-translate/core'; +import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { MockComponent, MockModule, MockProvider } from 'ng-mocks'; +import { of } from 'rxjs'; +import { MockRouterLinkDirective } from '../../helpers/mocks/directive/mock-router-link.directive'; +import { MockRouter } from '../../helpers/mocks/mock-router'; +import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; +import { ArtemisTestModule } from '../../test.module'; +import { FAQService } from 'app/faq/faq.service'; +import { Faq } from 'app/entities/faq.model'; +import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; +import { MockResizeObserver } from '../../helpers/mocks/service/mock-resize-observer'; + +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { FAQComponent } from 'app/faq/faq.component'; +import { FAQCategory } from 'app/entities/faq-category.model'; +import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; + +describe('FaqComponent', () => { + let faqComponentFixture: ComponentFixture; + let faqComponent: FAQComponent; + + let faqService: FAQService; + + let faq1: Faq; + let faq2: Faq; + let faq3: Faq; + + beforeEach(() => { + faq1 = new Faq(); + faq1.id = 1; + faq1.questionTitle = 'questionTitle'; + faq1.questionAnswer = 'questionAnswer'; + faq1.categories = [new FAQCategory('category1', '#94a11c')]; + + faq2 = new Faq(); + faq2.id = 2; + faq2.questionTitle = 'questionTitle'; + faq2.questionAnswer = 'questionAnswer'; + faq2.categories = [new FAQCategory('category2', '#0ab84f')]; + + faq3 = new Faq(); + faq3.id = 3; + faq3.questionTitle = 'questionTitle'; + faq3.questionAnswer = 'questionAnswer'; + faq3.categories = [new FAQCategory('category3', '#0ab84f')]; + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, ArtemisMarkdownEditorModule, MockModule(BrowserAnimationsModule)], + declarations: [FAQComponent, MockRouterLinkDirective, MockComponent(CustomExerciseCategoryBadgeComponent)], + providers: [ + { provide: TranslateService, useClass: MockTranslateService }, + { provide: Router, useClass: MockRouter }, + { + provide: ActivatedRoute, + useValue: { + parent: { + data: of({ course: { id: 1 } }), + }, + queryParams: of({ + params: {}, + }), + snapshot: { + paramMap: convertToParamMap({ + courseId: '1', + }), + }, + }, + }, + MockProvider(FAQService, { + findAllByCourseId: () => { + return of( + new HttpResponse({ + body: [faq1, faq2, faq3], + status: 200, + }), + ); + }, + delete: () => { + return of(new HttpResponse({ status: 200 })); + }, + findAllCategoriesByCourseId: () => { + return of( + new HttpResponse({ + body: [], + status: 200, + }), + ); + }, + }), + ], + }).compileComponents(); + + global.ResizeObserver = jest.fn().mockImplementation((callback: ResizeObserverCallback) => { + return new MockResizeObserver(callback); + }); + faqComponentFixture = TestBed.createComponent(FAQComponent); + faqComponent = faqComponentFixture.componentInstance; + + faqService = TestBed.inject(FAQService); + + faqComponentFixture.detectChanges(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should delete faq', () => { + const deleteSpy = jest.spyOn(faqService, 'delete'); + + faqComponentFixture.detectChanges(); + faqComponent.deleteFaq(faq1.id!); + + expect(deleteSpy).toHaveBeenCalledOnce(); + expect(deleteSpy).toHaveBeenCalledWith(faq1.id!); + expect(faqComponent.faqs).toBeArrayOfSize(2); + expect(faqComponent.faqs).not.toContain(faq1); + expect(faqComponent.filteredFaqs).toEqual(faqComponent.faqs); + }); + + it('should filter for past lectures', () => { + faqComponentFixture.detectChanges(); + faqComponent.toggleFilters('category1'); + expect(faqComponent.filteredFaqs).toBeArrayOfSize(1); + expect(faqComponent.filteredFaqs).toContainAllValues([faq1]); + }); +}); diff --git a/src/test/javascript/spec/service/faq.service.spec.ts b/src/test/javascript/spec/service/faq.service.spec.ts index 20ad9e87e448..a64d278d6987 100644 --- a/src/test/javascript/spec/service/faq.service.spec.ts +++ b/src/test/javascript/spec/service/faq.service.spec.ts @@ -9,12 +9,12 @@ import { TranslateService } from '@ngx-translate/core'; import { MockTranslateService } from '../helpers/mocks/service/mock-translate.service'; import { Course } from 'app/entities/course.model'; import { Faq, FaqState } from 'app/entities/faq.model'; -import { FaqCategory } from 'app/entities/faq-category.model'; -import { FaqService } from 'app/faq/faq.service'; +import { FAQCategory } from 'app/entities/faq-category.model'; +import { FAQService } from 'app/faq/faq.service'; describe('Faq Service', () => { let httpMock: HttpTestingController; - let service: FaqService; + let service: FAQService; const resourceUrl = 'api/faqs'; let expectedResult: any; let elemDefault: Faq; @@ -28,7 +28,7 @@ describe('Faq Service', () => { { provide: TranslateService, useClass: MockTranslateService }, ], }); - service = TestBed.inject(FaqService); + service = TestBed.inject(FAQService); httpMock = TestBed.inject(HttpTestingController); expectedResult = {} as HttpResponse; @@ -80,9 +80,9 @@ describe('Faq Service', () => { const category = { color: '#6ae8ac', category: 'category1', - } as FaqCategory; + } as FAQCategory; const returnedFromService = { ...elemDefault, categories: [JSON.stringify(category)] }; - const expected = { ...elemDefault, categories: [new FaqCategory('category1', '#6ae8ac')] }; + const expected = { ...elemDefault, categories: [new FAQCategory('category1', '#6ae8ac')] }; const faqId = elemDefault.id!; service .find(faqId) @@ -100,9 +100,9 @@ describe('Faq Service', () => { const category = { color: '#6ae8ac', category: 'category1', - } as FaqCategory; + } as FAQCategory; const returnedFromService = [{ ...elemDefault, categories: [JSON.stringify(category)] }]; - const expected = [{ ...elemDefault, categories: [new FaqCategory('category1', '#6ae8ac')] }]; + const expected = [{ ...elemDefault, categories: [new FAQCategory('category1', '#6ae8ac')] }]; const courseId = 1; service .findAllByCourseId(courseId) @@ -120,7 +120,7 @@ describe('Faq Service', () => { const category = { color: '#6ae8ac', category: 'category1', - } as FaqCategory; + } as FAQCategory; const returnedFromService = { categories: [JSON.stringify(category)] }; const expected = { ...returnedFromService }; const courseId = 1; From afe6ea1362a58f907c32b4e2d2e25d5eaba74ea0 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Fri, 20 Sep 2024 11:32:49 +0200 Subject: [PATCH 60/68] Rename FAQ --- src/main/webapp/app/entities/faq.model.ts | 4 +- .../webapp/app/faq/faq-update.component.ts | 14 +++---- src/main/webapp/app/faq/faq.component.ts | 16 +++---- src/main/webapp/app/faq/faq.routes.ts | 12 +++--- src/main/webapp/app/faq/faq.service.ts | 42 +++++++++---------- .../course-faq-accordion-component.ts | 4 +- .../course-faq/course-faq.component.ts | 14 +++---- .../faq/faq-update.component.spec.ts | 12 +++--- .../spec/component/faq/faq.component.spec.ts | 14 +++---- .../spec/service/faq.service.spec.ts | 8 ++-- 10 files changed, 68 insertions(+), 72 deletions(-) diff --git a/src/main/webapp/app/entities/faq.model.ts b/src/main/webapp/app/entities/faq.model.ts index ab9cadc0664b..24f02a31d360 100644 --- a/src/main/webapp/app/entities/faq.model.ts +++ b/src/main/webapp/app/entities/faq.model.ts @@ -8,13 +8,11 @@ export enum FaqState { PROPOSED, } -export class Faq implements BaseEntity { +export class FAQ implements BaseEntity { public id?: number; public questionTitle?: string; public questionAnswer?: string; public faqState?: FaqState; public course?: Course; public categories?: FAQCategory[]; - - constructor() {} } diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index 1db317441090..bba31b0f6ddf 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -8,7 +8,7 @@ import { onError } from 'app/shared/util/global.utils'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { faBan, faQuestionCircle, faSave } from '@fortawesome/free-solid-svg-icons'; import { FormulaAction } from 'app/shared/monaco-editor/model/actions/formula.action'; -import { Faq } from 'app/entities/faq.model'; +import { FAQ } from 'app/entities/faq.model'; import { FAQService } from 'app/faq/faq.service'; import { TranslateService } from '@ngx-translate/core'; import { FAQCategory } from 'app/entities/faq-category.model'; @@ -26,7 +26,7 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo imports: [ArtemisSharedModule, ArtemisSharedComponentModule, ArtemisMarkdownEditorModule, ArtemisCategorySelectorModule], }) export class FAQUpdateComponent implements OnInit { - faq: Faq; + faq: FAQ; isSaving: boolean; existingCategories: FAQCategory[] = []; faqCategories: FAQCategory[] = []; @@ -56,7 +56,7 @@ export class FAQUpdateComponent implements OnInit { this.activatedRoute.parent?.data.subscribe((data) => { // Create a new faq to use unless we fetch an existing faq const faq = data['faq']; - this.faq = faq ?? new Faq(); + this.faq = faq ?? new FAQ(); const course = data['course']; if (course) { this.faq.course = course; @@ -94,9 +94,9 @@ export class FAQUpdateComponent implements OnInit { /** * @param result The Http response from the server */ - protected subscribeToSaveResponse(result: Observable>) { + protected subscribeToSaveResponse(result: Observable>) { result.subscribe({ - next: (response: HttpResponse) => this.onSaveSuccess(response.body!), + next: (response: HttpResponse) => this.onSaveSuccess(response.body!), error: (error: HttpErrorResponse) => this.onSaveError(error), }); } @@ -104,10 +104,10 @@ export class FAQUpdateComponent implements OnInit { /** * Action on successful faq creation or edit */ - protected onSaveSuccess(faq: Faq) { + protected onSaveSuccess(faq: FAQ) { if (!this.faq.id) { this.faqService.find(faq.id!).subscribe({ - next: (response: HttpResponse) => { + next: (response: HttpResponse) => { this.isSaving = false; const faqBody = response.body; if (faqBody) { diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index 918aa7a571a2..72e85352fdba 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { Faq } from 'app/entities/faq.model'; +import { FAQ } from 'app/entities/faq.model'; import { faEdit, faFile, faFileExport, faFileImport, faFilter, faPencilAlt, faPlus, faPuzzlePiece, faSort, faTrash } from '@fortawesome/free-solid-svg-icons'; import { Subject } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -24,8 +24,8 @@ import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; imports: [ArtemisSharedModule, CustomExerciseCategoryBadgeComponent, ArtemisSharedComponentModule, ArtemisMarkdownModule], }) export class FAQComponent implements OnInit, OnDestroy { - faqs: Faq[]; - filteredFaqs: Faq[]; + faqs: FAQ[]; + filteredFaqs: FAQ[]; existingCategories: FAQCategory[]; courseId: number; @@ -70,7 +70,7 @@ export class FAQComponent implements OnInit, OnDestroy { this.dialogErrorSource.complete(); } - trackId(index: number, item: Faq) { + trackId(index: number, item: FAQ) { return item.id; } @@ -88,12 +88,12 @@ export class FAQComponent implements OnInit, OnDestroy { } toggleFilters(category: string) { - this.activeFilters = FAQService.toggleFilter(category, this.activeFilters); + this.activeFilters = this.faqService.toggleFilter(category, this.activeFilters); this.applyFilters(); } private applyFilters(): void { - this.filteredFaqs = FAQService.applyFilters(this.activeFilters, this.faqs); + this.filteredFaqs = this.faqService.applyFilters(this.activeFilters, this.faqs); } sortRows() { @@ -103,9 +103,9 @@ export class FAQComponent implements OnInit, OnDestroy { private loadAll() { this.faqService .findAllByCourseId(this.courseId) - .pipe(map((res: HttpResponse) => res.body)) + .pipe(map((res: HttpResponse) => res.body)) .subscribe({ - next: (res: Faq[]) => { + next: (res: FAQ[]) => { this.faqs = res; this.applyFilters(); }, diff --git a/src/main/webapp/app/faq/faq.routes.ts b/src/main/webapp/app/faq/faq.routes.ts index 312319c60247..ecda5cf3a525 100644 --- a/src/main/webapp/app/faq/faq.routes.ts +++ b/src/main/webapp/app/faq/faq.routes.ts @@ -9,22 +9,22 @@ import { CourseManagementResolve } from 'app/course/manage/course-management-res import { CourseManagementTabBarComponent } from 'app/course/manage/course-management-tab-bar/course-management-tab-bar.component'; import { FAQComponent } from 'app/faq/faq.component'; import { FAQService } from 'app/faq/faq.service'; -import { Faq } from 'app/entities/faq.model'; +import { FAQ } from 'app/entities/faq.model'; import { FAQUpdateComponent } from 'app/faq/faq-update.component'; @Injectable({ providedIn: 'root' }) -export class FAQResolve implements Resolve { +export class FAQResolve implements Resolve { constructor(private faqService: FAQService) {} - resolve(route: ActivatedRouteSnapshot): Observable { + resolve(route: ActivatedRouteSnapshot): Observable { const faqId = route.params['faqId']; if (faqId) { return this.faqService.find(faqId).pipe( - filter((response: HttpResponse) => response.ok), - map((faq: HttpResponse) => faq.body!), + filter((response: HttpResponse) => response.ok), + map((faq: HttpResponse) => faq.body!), ); } - return of(new Faq()); + return of(new FAQ()); } } diff --git a/src/main/webapp/app/faq/faq.service.ts b/src/main/webapp/app/faq/faq.service.ts index e7384713623c..53d88292f900 100644 --- a/src/main/webapp/app/faq/faq.service.ts +++ b/src/main/webapp/app/faq/faq.service.ts @@ -2,12 +2,12 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { Faq, FaqState } from 'app/entities/faq.model'; +import { FAQ, FaqState } from 'app/entities/faq.model'; import { FAQCategory } from 'app/entities/faq-category.model'; import { AlertService } from 'app/core/util/alert.service'; -type EntityResponseType = HttpResponse; -type EntityArrayResponseType = HttpResponse; +type EntityResponseType = HttpResponse; +type EntityArrayResponseType = HttpResponse; @Injectable({ providedIn: 'root' }) export class FAQService { @@ -18,19 +18,19 @@ export class FAQService { protected alertService: AlertService, ) {} - create(faq: Faq): Observable { + create(faq: FAQ): Observable { const copy = FAQService.convertFaqFromClient(faq); faq.faqState = FaqState.ACCEPTED; - return this.http.post(`api/faqs`, copy, { observe: 'response' }).pipe( + return this.http.post(`api/faqs`, copy, { observe: 'response' }).pipe( map((res: EntityResponseType) => { return res; }), ); } - update(faq: Faq): Observable { + update(faq: FAQ): Observable { const copy = FAQService.convertFaqFromClient(faq); - return this.http.put(`api/faqs/${faq.id}`, copy, { observe: 'response' }).pipe( + return this.http.put(`api/faqs/${faq.id}`, copy, { observe: 'response' }).pipe( map((res: EntityResponseType) => { return res; }), @@ -38,12 +38,12 @@ export class FAQService { } find(faqId: number): Observable { - return this.http.get(`api/faqs/${faqId}`, { observe: 'response' }).pipe(map((res: EntityResponseType) => FAQService.convertFaqCategoriesFromServer(res))); + return this.http.get(`api/faqs/${faqId}`, { observe: 'response' }).pipe(map((res: EntityResponseType) => FAQService.convertFaqCategoriesFromServer(res))); } findAllByCourseId(courseId: number): Observable { return this.http - .get(this.resourceUrl + `/${courseId}/faqs`, { + .get(`${this.resourceUrl}/${courseId}/faqs`, { observe: 'response', }) .pipe(map((res: EntityArrayResponseType) => FAQService.convertFaqCategoryArrayFromServer(res))); @@ -54,7 +54,7 @@ export class FAQService { } findAllCategoriesByCourseId(courseId: number) { - return this.http.get(this.resourceUrl + `/${courseId}/faq-categories`, { + return this.http.get(`${this.resourceUrl}/${courseId}/faq-categories`, { observe: 'response', }); } @@ -73,7 +73,7 @@ export class FAQService { * Converts a faqs categories into a json string (to send them to the server). Does nothing if no categories exist * @param faq the faq */ - static stringifyFaqCategories(faq: Faq) { + static stringifyFaqCategories(faq: FAQ) { return faq.categories?.map((category) => JSON.stringify(category) as unknown as FAQCategory); } @@ -85,7 +85,7 @@ export class FAQService { * Converts the faq category json strings into FaqCategory objects (if it exists). * @param res the response */ - static convertFaqCategoryArrayFromServer(res: EART): EART { + static convertFaqCategoryArrayFromServer(res: EART): EART { if (res.body) { res.body.forEach((faq: E) => FAQService.parseFaqCategories(faq)); } @@ -96,7 +96,7 @@ export class FAQService { * Parses the faq categories JSON string into {@link FAQCategory} objects. * @param faq - the faq */ - static parseFaqCategories(faq?: Faq) { + static parseFaqCategories(faq?: FAQ) { if (faq?.categories) { faq.categories = faq.categories.map((category) => { const categoryObj = JSON.parse(category as unknown as string); @@ -107,17 +107,15 @@ export class FAQService { /** * Prepare client-faq to be uploaded to the server - * @param { Faq } faq - faq that will be modified + * @param { FAQ } faq - faq that will be modified */ - static convertFaqFromClient(faq: F): Faq { - const copy = Object.assign(faq, {}); + static convertFaqFromClient(faq: F): FAQ { + const copy = Object.assign({}, faq); copy.categories = FAQService.stringifyFaqCategories(copy); - if (copy.categories) { - } return copy; } - static toggleFilter(category: string, activeFilters: Set) { + toggleFilter(category: string, activeFilters: Set) { if (activeFilters.has(category)) { activeFilters.delete(category); return activeFilters; @@ -127,8 +125,8 @@ export class FAQService { } } - static applyFilters(activeFilters: Set, faqs: Faq[]): Faq[] { - let filteredFaq: Faq[]; + applyFilters(activeFilters: Set, faqs: FAQ[]): FAQ[] { + let filteredFaq: FAQ[]; if (activeFilters.size === 0) { // If no filters selected, show all faqs filteredFaq = faqs; @@ -138,7 +136,7 @@ export class FAQService { return filteredFaq; } - public static hasFilteredCategory(faq: Faq, filteredCategory: Set) { + hasFilteredCategory(faq: FAQ, filteredCategory: Set) { const categories = faq.categories?.map((category) => category.category); if (categories) { return categories.some((category) => filteredCategory.has(category!)); diff --git a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts index 08a01b290fd9..d0e3cba99120 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts @@ -1,6 +1,6 @@ import { Component, OnDestroy, input } from '@angular/core'; import { TranslateDirective } from 'app/shared/language/translate.directive'; -import { Faq } from 'app/entities/faq.model'; +import { FAQ } from 'app/entities/faq.model'; import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; import { Subject } from 'rxjs/internal/Subject'; import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; @@ -15,7 +15,7 @@ import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; }) export class CourseFaqAccordionComponent implements OnDestroy { private ngUnsubscribe = new Subject(); - faq = input.required(); + faq = input.required(); ngOnDestroy(): void { this.ngUnsubscribe.next(); diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts index 4354af225fca..9f7a193c96de 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -9,7 +9,7 @@ import { SidebarData } from 'app/types/sidebar'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { CourseFaqAccordionComponent } from 'app/overview/course-faq/course-faq-accordion-component'; -import { Faq } from 'app/entities/faq.model'; +import { FAQ } from 'app/entities/faq.model'; import { FAQService } from 'app/faq/faq.service'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; @@ -32,9 +32,9 @@ export class CourseFaqComponent implements OnInit, OnDestroy { private parentParamSubscription: Subscription; courseId: number; - faqs: Faq[]; + faqs: FAQ[]; - filteredFaq: Faq[]; + filteredFaq: FAQ[]; existingCategories: FAQCategory[]; activeFilters = new Set(); @@ -74,9 +74,9 @@ export class CourseFaqComponent implements OnInit, OnDestroy { private loadFaqs() { this.faqService .findAllByCourseId(this.courseId) - .pipe(map((res: HttpResponse) => res.body)) + .pipe(map((res: HttpResponse) => res.body)) .subscribe({ - next: (res: Faq[]) => { + next: (res: FAQ[]) => { this.faqs = res; this.applyFilters(); }, @@ -91,11 +91,11 @@ export class CourseFaqComponent implements OnInit, OnDestroy { } toggleFilters(category: string) { - this.activeFilters = FAQService.toggleFilter(category, this.activeFilters); + this.activeFilters = this.faqService.toggleFilter(category, this.activeFilters); this.applyFilters(); } private applyFilters(): void { - this.filteredFaq = FAQService.applyFilters(this.activeFilters, this.faqs); + this.filteredFaq = this.faqService.applyFilters(this.activeFilters, this.faqs); } } diff --git a/src/test/javascript/spec/component/faq/faq-update.component.spec.ts b/src/test/javascript/spec/component/faq/faq-update.component.spec.ts index e99c4ebd2244..ed0a8a293f1b 100644 --- a/src/test/javascript/spec/component/faq/faq-update.component.spec.ts +++ b/src/test/javascript/spec/component/faq/faq-update.component.spec.ts @@ -11,7 +11,7 @@ import { MockTranslateService } from '../../helpers/mocks/service/mock-translate import { ArtemisTestModule } from '../../test.module'; import { FAQUpdateComponent } from 'app/faq/faq-update.component'; import { FAQService } from 'app/faq/faq.service'; -import { Faq } from 'app/entities/faq.model'; +import { FAQ } from 'app/entities/faq.model'; import { MonacoEditorComponent } from 'app/shared/monaco-editor/monaco-editor.component'; import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.module'; import { MockResizeObserver } from '../../helpers/mocks/service/mock-resize-observer'; @@ -70,7 +70,7 @@ describe('FaqUpdateComponent', () => { }); it('should create faq', fakeAsync(() => { - faqUpdateComponent.faq = { questionTitle: 'test1' } as Faq; + faqUpdateComponent.faq = { questionTitle: 'test1' } as FAQ; const createSpy = jest.spyOn(faqService, 'create').mockReturnValue( of( @@ -81,7 +81,7 @@ describe('FaqUpdateComponent', () => { course: { id: 1, }, - } as Faq, + } as FAQ, }), ), ); @@ -98,10 +98,10 @@ describe('FaqUpdateComponent', () => { activatedRoute.parent!.data = of({ course: { id: 1 }, faq: { id: 6 } }); faqUpdateComponentFixture.detectChanges(); - faqUpdateComponent.faq = { id: 6, questionTitle: 'test1Updated' } as Faq; + faqUpdateComponent.faq = { id: 6, questionTitle: 'test1Updated' } as FAQ; const updateSpy = jest.spyOn(faqService, 'update').mockReturnValue( - of>( + of>( new HttpResponse({ body: { id: 6, @@ -109,7 +109,7 @@ describe('FaqUpdateComponent', () => { course: { id: 1, }, - } as Faq, + } as FAQ, }), ), ); diff --git a/src/test/javascript/spec/component/faq/faq.component.spec.ts b/src/test/javascript/spec/component/faq/faq.component.spec.ts index 1b6a46238f32..2aa9ae793c8b 100644 --- a/src/test/javascript/spec/component/faq/faq.component.spec.ts +++ b/src/test/javascript/spec/component/faq/faq.component.spec.ts @@ -9,7 +9,7 @@ import { MockRouter } from '../../helpers/mocks/mock-router'; import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; import { ArtemisTestModule } from '../../test.module'; import { FAQService } from 'app/faq/faq.service'; -import { Faq } from 'app/entities/faq.model'; +import { FAQ } from 'app/entities/faq.model'; import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; import { MockResizeObserver } from '../../helpers/mocks/service/mock-resize-observer'; @@ -24,24 +24,24 @@ describe('FaqComponent', () => { let faqService: FAQService; - let faq1: Faq; - let faq2: Faq; - let faq3: Faq; + let faq1: FAQ; + let faq2: FAQ; + let faq3: FAQ; beforeEach(() => { - faq1 = new Faq(); + faq1 = new FAQ(); faq1.id = 1; faq1.questionTitle = 'questionTitle'; faq1.questionAnswer = 'questionAnswer'; faq1.categories = [new FAQCategory('category1', '#94a11c')]; - faq2 = new Faq(); + faq2 = new FAQ(); faq2.id = 2; faq2.questionTitle = 'questionTitle'; faq2.questionAnswer = 'questionAnswer'; faq2.categories = [new FAQCategory('category2', '#0ab84f')]; - faq3 = new Faq(); + faq3 = new FAQ(); faq3.id = 3; faq3.questionTitle = 'questionTitle'; faq3.questionAnswer = 'questionAnswer'; diff --git a/src/test/javascript/spec/service/faq.service.spec.ts b/src/test/javascript/spec/service/faq.service.spec.ts index a64d278d6987..a37994c7b907 100644 --- a/src/test/javascript/spec/service/faq.service.spec.ts +++ b/src/test/javascript/spec/service/faq.service.spec.ts @@ -8,7 +8,7 @@ import { MockSyncStorage } from '../helpers/mocks/service/mock-sync-storage.serv import { TranslateService } from '@ngx-translate/core'; import { MockTranslateService } from '../helpers/mocks/service/mock-translate.service'; import { Course } from 'app/entities/course.model'; -import { Faq, FaqState } from 'app/entities/faq.model'; +import { FAQ, FaqState } from 'app/entities/faq.model'; import { FAQCategory } from 'app/entities/faq-category.model'; import { FAQService } from 'app/faq/faq.service'; @@ -17,7 +17,7 @@ describe('Faq Service', () => { let service: FAQService; const resourceUrl = 'api/faqs'; let expectedResult: any; - let elemDefault: Faq; + let elemDefault: FAQ; beforeEach(() => { TestBed.configureTestingModule({ @@ -31,8 +31,8 @@ describe('Faq Service', () => { service = TestBed.inject(FAQService); httpMock = TestBed.inject(HttpTestingController); - expectedResult = {} as HttpResponse; - elemDefault = new Faq(); + expectedResult = {} as HttpResponse; + elemDefault = new FAQ(); elemDefault.questionTitle = 'Title'; elemDefault.course = new Course(); elemDefault.questionAnswer = 'Answer'; From 1dfdac3a1d235b5755c94c6113122a58f594ab0a Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Fri, 20 Sep 2024 13:27:03 +0200 Subject: [PATCH 61/68] Fixed creation bug and made category filter working as expected --- .../communication/web/FaqResource.java | 3 +- src/main/webapp/app/entities/faq.model.ts | 4 +-- .../webapp/app/faq/faq-update.component.scss | 4 --- .../webapp/app/faq/faq-update.component.ts | 8 ++--- src/main/webapp/app/faq/faq.component.html | 34 ++++++++++--------- src/main/webapp/app/faq/faq.component.ts | 2 ++ src/main/webapp/app/faq/faq.service.ts | 4 +-- .../course-faq/course-faq.component.html | 34 ++++++++++--------- .../course-faq/course-faq.component.ts | 2 ++ .../spec/component/faq/faq.component.spec.ts | 3 +- .../spec/service/faq.service.spec.ts | 4 +-- 11 files changed, 53 insertions(+), 49 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java index d828bf551f24..8da9e7d0f1c9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java @@ -83,7 +83,8 @@ public ResponseEntity createFaq(@RequestBody Faq faq) throws URISyntaxExcep /** * PUT /faqs/{faqId} : Updates an existing faq. * - * @param faq the faq to update + * @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 */ diff --git a/src/main/webapp/app/entities/faq.model.ts b/src/main/webapp/app/entities/faq.model.ts index 24f02a31d360..35736ba9296d 100644 --- a/src/main/webapp/app/entities/faq.model.ts +++ b/src/main/webapp/app/entities/faq.model.ts @@ -2,7 +2,7 @@ import { BaseEntity } from 'app/shared/model/base-entity'; import { Course } from 'app/entities/course.model'; import { FAQCategory } from './faq-category.model'; -export enum FaqState { +export enum FAQState { ACCEPTED, REJECTED, PROPOSED, @@ -12,7 +12,7 @@ export class FAQ implements BaseEntity { public id?: number; public questionTitle?: string; public questionAnswer?: string; - public faqState?: FaqState; + public faqState?: FAQState; public course?: Course; public categories?: FAQCategory[]; } diff --git a/src/main/webapp/app/faq/faq-update.component.scss b/src/main/webapp/app/faq/faq-update.component.scss index 0e27c3189cd2..c8c63e8a710c 100644 --- a/src/main/webapp/app/faq/faq-update.component.scss +++ b/src/main/webapp/app/faq/faq-update.component.scss @@ -1,7 +1,3 @@ .markdown-editor { height: 350px; } - - - - diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index bba31b0f6ddf..f8bce502d5f5 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -8,7 +8,7 @@ import { onError } from 'app/shared/util/global.utils'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { faBan, faQuestionCircle, faSave } from '@fortawesome/free-solid-svg-icons'; import { FormulaAction } from 'app/shared/monaco-editor/model/actions/formula.action'; -import { FAQ } from 'app/entities/faq.model'; +import { FAQ, FAQState } from 'app/entities/faq.model'; import { FAQService } from 'app/faq/faq.service'; import { TranslateService } from '@ngx-translate/core'; import { FAQCategory } from 'app/entities/faq-category.model'; @@ -28,8 +28,8 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo export class FAQUpdateComponent implements OnInit { faq: FAQ; isSaving: boolean; - existingCategories: FAQCategory[] = []; - faqCategories: FAQCategory[] = []; + existingCategories: FAQCategory[]; + faqCategories: FAQCategory[]; domainActionsDescription = [new FormulaAction()]; @@ -86,7 +86,7 @@ export class FAQUpdateComponent implements OnInit { if (this.faq.id !== undefined) { this.subscribeToSaveResponse(this.faqService.update(this.faq)); } else { - // Newly created faq must have a channel name, which cannot be undefined + this.faq.faqState = FAQState.ACCEPTED; this.subscribeToSaveResponse(this.faqService.create(this.faq)); } } diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index eceb15b6b8af..7b0f1312fc12 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -17,22 +17,24 @@

-
    - @for (category of existingCategories; track category) { -
  • - -
  • - } -
+ @if (hasCategories) { +
    + @for (category of existingCategories; track category) { +
  • + +
  • + } +
+ }

diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index 72e85352fdba..0ca4ed856d3c 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -28,6 +28,7 @@ export class FAQComponent implements OnInit, OnDestroy { filteredFaqs: FAQ[]; existingCategories: FAQCategory[]; courseId: number; + hasCategories: boolean = false; private dialogErrorSource = new Subject(); dialogError$ = this.dialogErrorSource.asObservable(); @@ -116,6 +117,7 @@ export class FAQComponent implements OnInit, OnDestroy { private loadCourseFaqCategories(courseId: number) { loadCourseFaqCategories(courseId, this.alertService, this.faqService).subscribe((existingCategories) => { this.existingCategories = existingCategories; + this.hasCategories = existingCategories.length > 0; }); } } diff --git a/src/main/webapp/app/faq/faq.service.ts b/src/main/webapp/app/faq/faq.service.ts index 53d88292f900..ab69a76ea842 100644 --- a/src/main/webapp/app/faq/faq.service.ts +++ b/src/main/webapp/app/faq/faq.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { FAQ, FaqState } from 'app/entities/faq.model'; +import { FAQ, FAQState } from 'app/entities/faq.model'; import { FAQCategory } from 'app/entities/faq-category.model'; import { AlertService } from 'app/core/util/alert.service'; @@ -20,7 +20,7 @@ export class FAQService { create(faq: FAQ): Observable { const copy = FAQService.convertFaqFromClient(faq); - faq.faqState = FaqState.ACCEPTED; + faq.faqState = FAQState.ACCEPTED; return this.http.post(`api/faqs`, copy, { observe: 'response' }).pipe( map((res: EntityResponseType) => { return res; diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.html b/src/main/webapp/app/overview/course-faq/course-faq.component.html index 86bfad84662b..8989aa5d897c 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.html +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.html @@ -5,22 +5,24 @@ -
    - @for (category of existingCategories; track category) { -
  • - -
  • - } -
+ @if (hasCategories) { +
    + @for (category of existingCategories; track category) { +
  • + +
  • + } +
+ }
diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts index 9f7a193c96de..eccc89768a1f 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -39,6 +39,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy { activeFilters = new Set(); sidebarData: SidebarData; + hasCategories = false; isCollapsed = false; isProduction = true; isTestServer = false; @@ -68,6 +69,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy { private loadCourseExerciseCategories(courseId: number) { loadCourseFaqCategories(courseId, this.alertService, this.faqService).subscribe((existingCategories) => { this.existingCategories = existingCategories; + this.hasCategories = existingCategories.length > 0; }); } diff --git a/src/test/javascript/spec/component/faq/faq.component.spec.ts b/src/test/javascript/spec/component/faq/faq.component.spec.ts index 2aa9ae793c8b..434a5e3f9a86 100644 --- a/src/test/javascript/spec/component/faq/faq.component.spec.ts +++ b/src/test/javascript/spec/component/faq/faq.component.spec.ts @@ -99,7 +99,6 @@ describe('FaqComponent', () => { faqComponent = faqComponentFixture.componentInstance; faqService = TestBed.inject(FAQService); - faqComponentFixture.detectChanges(); }); @@ -120,7 +119,7 @@ describe('FaqComponent', () => { expect(faqComponent.filteredFaqs).toEqual(faqComponent.faqs); }); - it('should filter for past lectures', () => { + it('should filter for faqs lectures', () => { faqComponentFixture.detectChanges(); faqComponent.toggleFilters('category1'); expect(faqComponent.filteredFaqs).toBeArrayOfSize(1); diff --git a/src/test/javascript/spec/service/faq.service.spec.ts b/src/test/javascript/spec/service/faq.service.spec.ts index a37994c7b907..b258bcf680ad 100644 --- a/src/test/javascript/spec/service/faq.service.spec.ts +++ b/src/test/javascript/spec/service/faq.service.spec.ts @@ -8,7 +8,7 @@ import { MockSyncStorage } from '../helpers/mocks/service/mock-sync-storage.serv import { TranslateService } from '@ngx-translate/core'; import { MockTranslateService } from '../helpers/mocks/service/mock-translate.service'; import { Course } from 'app/entities/course.model'; -import { FAQ, FaqState } from 'app/entities/faq.model'; +import { FAQ, FAQState } from 'app/entities/faq.model'; import { FAQCategory } from 'app/entities/faq-category.model'; import { FAQService } from 'app/faq/faq.service'; @@ -37,7 +37,7 @@ describe('Faq Service', () => { elemDefault.course = new Course(); elemDefault.questionAnswer = 'Answer'; elemDefault.id = 1; - elemDefault.faqState = FaqState.ACCEPTED; + elemDefault.faqState = FAQState.ACCEPTED; }); afterEach(() => { From 8ae8c86b28db075730786862e9c26ff028af6b7d Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Fri, 20 Sep 2024 15:16:03 +0200 Subject: [PATCH 62/68] added tests, one is still not working --- .../webapp/app/faq/faq-update.component.ts | 4 +-- .../spec/component/faq/faq.component.spec.ts | 22 +++++++----- .../spec/service/faq.service.spec.ts | 35 +++++++++++++++++++ 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index f8bce502d5f5..8c302fc7fb9b 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -62,9 +62,7 @@ export class FAQUpdateComponent implements OnInit { this.faq.course = course; this.loadCourseFaqCategories(course.id); } - if (faq.categories) { - this.faqCategories = faq.categories; - } + this.faqCategories = faq?.categories ? faq.categories : []; }); } diff --git a/src/test/javascript/spec/component/faq/faq.component.spec.ts b/src/test/javascript/spec/component/faq/faq.component.spec.ts index 434a5e3f9a86..7c8d5cd21f65 100644 --- a/src/test/javascript/spec/component/faq/faq.component.spec.ts +++ b/src/test/javascript/spec/component/faq/faq.component.spec.ts @@ -88,6 +88,9 @@ describe('FaqComponent', () => { }), ); }, + applyFilters: () => { + return [faq2, faq3]; + }, }), ], }).compileComponents(); @@ -106,6 +109,16 @@ describe('FaqComponent', () => { jest.restoreAllMocks(); }); + it('should fetch faqs when initialized', () => { + const findAllSpy = jest.spyOn(faqService, 'findAllByCourseId'); + + faqComponentFixture.detectChanges(); + //is actually called when debugging, i dont get why it is 0. Need help + expect(findAllSpy).toHaveBeenCalledOnce(); + expect(findAllSpy).toHaveBeenCalledWith(1); + expect(faqComponent.faqs).toBeArrayOfSize(3); + }); + it('should delete faq', () => { const deleteSpy = jest.spyOn(faqService, 'delete'); @@ -116,13 +129,6 @@ describe('FaqComponent', () => { expect(deleteSpy).toHaveBeenCalledWith(faq1.id!); expect(faqComponent.faqs).toBeArrayOfSize(2); expect(faqComponent.faqs).not.toContain(faq1); - expect(faqComponent.filteredFaqs).toEqual(faqComponent.faqs); - }); - - it('should filter for faqs lectures', () => { - faqComponentFixture.detectChanges(); - faqComponent.toggleFilters('category1'); - expect(faqComponent.filteredFaqs).toBeArrayOfSize(1); - expect(faqComponent.filteredFaqs).toContainAllValues([faq1]); + expect(faqComponent.faqs).toEqual(faqComponent.filteredFaqs); }); }); diff --git a/src/test/javascript/spec/service/faq.service.spec.ts b/src/test/javascript/spec/service/faq.service.spec.ts index b258bcf680ad..75550480aea0 100644 --- a/src/test/javascript/spec/service/faq.service.spec.ts +++ b/src/test/javascript/spec/service/faq.service.spec.ts @@ -135,5 +135,40 @@ describe('Faq Service', () => { req.flush(returnedFromService); expect(expectedResult.body).toEqual(expected); }); + + it('should set add active filter correctly', async () => { + let activeFilters = new Set(); + activeFilters = service.toggleFilter('category1', activeFilters); + + expect(activeFilters).toContain('category1'); + expect(activeFilters.size).toBe(1); + }); + + it('should remove active filter correctly', async () => { + let activeFilters = new Set(); + activeFilters.add('category1'); + activeFilters = service.toggleFilter('category1', activeFilters); + + expect(activeFilters).not.toContain('category1'); + expect(activeFilters.size).toBe(0); + }); + + it('should apply faqFilter correctly', async () => { + const activeFilters = new Set(); + activeFilters.add('test'); + const faq1 = new FAQ(); + faq1.categories = [new FAQCategory('test', 'red'), new FAQCategory('test2', 'blue')]; + + const faq11 = new FAQ(); + faq11.categories = [new FAQCategory('test', 'red'), new FAQCategory('test2', 'blue')]; + + const faq2 = new FAQ(); + faq2.categories = [new FAQCategory('testing', 'red'), new FAQCategory('test2', 'blue')]; + let filteredFaq = [faq1, faq11, faq2]; + filteredFaq = service.applyFilters(activeFilters, filteredFaq); + + expect(filteredFaq).toBeArrayOfSize(2); + expect(filteredFaq).toContainAllValues([faq1, faq11]); + }); }); }); From ab73e5078ffaca2d267d30db33a62660f589b914 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Fri, 20 Sep 2024 15:40:46 +0200 Subject: [PATCH 63/68] tried to fix E2E tests --- .../playwright/e2e/course/CourseManagement.spec.ts | 3 +++ .../pageobjects/course/CourseCreationPage.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/test/playwright/e2e/course/CourseManagement.spec.ts b/src/test/playwright/e2e/course/CourseManagement.spec.ts index 8a7eacab4135..dc79eca9b785 100644 --- a/src/test/playwright/e2e/course/CourseManagement.spec.ts +++ b/src/test/playwright/e2e/course/CourseManagement.spec.ts @@ -23,6 +23,7 @@ const courseData = { editorGroupName: process.env.EDITOR_GROUP_NAME ?? '', instructorGroupName: process.env.INSTRUCTOR_GROUP_NAME ?? '', enableComplaints: true, + enableFaqs: true, maxComplaints: 5, maxTeamComplaints: 3, maxComplaintTimeDays: 6, @@ -100,6 +101,7 @@ test.describe('Course management', () => { await courseCreation.setCourseMaxPoints(courseData.maxPoints); await courseCreation.setProgrammingLanguage(courseData.programmingLanguage); await courseCreation.setEnableComplaints(courseData.enableComplaints); + await courseCreation.setEnableFaqs(courseData.enableFaqs); await courseCreation.setMaxComplaints(courseData.maxComplaints); await courseCreation.setMaxTeamComplaints(courseData.maxTeamComplaints); await courseCreation.setMaxComplaintsTimeDays(courseData.maxComplaintTimeDays); @@ -120,6 +122,7 @@ test.describe('Course management', () => { expect(courseBody.maxPoints).toBe(courseData.maxPoints); expect(courseBody.defaultProgrammingLanguage).toBe(courseData.programmingLanguage); expect(courseBody.complaintsEnabled).toBe(courseData.enableComplaints); + expect(courseBody.faqEnabled).toBe(courseData.enableFaqs); expect(courseBody.maxComplaints).toBe(courseData.maxComplaints); expect(courseBody.maxTeamComplaints).toBe(courseData.maxTeamComplaints); expect(courseBody.maxComplaintTimeDays).toBe(courseData.maxComplaintTimeDays); diff --git a/src/test/playwright/support/pageobjects/course/CourseCreationPage.ts b/src/test/playwright/support/pageobjects/course/CourseCreationPage.ts index 2048bdbed18d..e864f905903e 100644 --- a/src/test/playwright/support/pageobjects/course/CourseCreationPage.ts +++ b/src/test/playwright/support/pageobjects/course/CourseCreationPage.ts @@ -147,6 +147,19 @@ export class CourseCreationPage { } } + /** + * Sets if complaints are enabled + * @param complaints if complaints should be enabled + */ + async setEnableFaqs(faqs: boolean) { + const selector = this.page.locator('#field_faq_enabled'); + if (faqs) { + await selector.check(); + } else { + await selector.uncheck(); + } + } + /** * Sets maximum amount of complaints * @param maxComplaints the maximum complaints From 40e121355791e83f126ed78c0914eb5167c776b0 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Fri, 20 Sep 2024 16:21:20 +0200 Subject: [PATCH 64/68] coderabit --- src/main/webapp/app/faq/faq-update.component.ts | 2 -- src/main/webapp/app/faq/faq.service.ts | 9 +++------ .../spec/component/faq/faq-update.component.spec.ts | 4 ++-- .../javascript/spec/component/faq/faq.component.spec.ts | 4 ++-- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index 8c302fc7fb9b..9779268904fb 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -3,7 +3,6 @@ import { ActivatedRoute, Router } from '@angular/router'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { AlertService } from 'app/core/util/alert.service'; -import { CourseManagementService } from '../course/manage/course-management.service'; import { onError } from 'app/shared/util/global.utils'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { faBan, faQuestionCircle, faSave } from '@fortawesome/free-solid-svg-icons'; @@ -41,7 +40,6 @@ export class FAQUpdateComponent implements OnInit { constructor( protected alertService: AlertService, protected faqService: FAQService, - protected courseService: CourseManagementService, protected activatedRoute: ActivatedRoute, private navigationUtilService: ArtemisNavigationUtilService, private router: Router, diff --git a/src/main/webapp/app/faq/faq.service.ts b/src/main/webapp/app/faq/faq.service.ts index ab69a76ea842..7e690ba0df01 100644 --- a/src/main/webapp/app/faq/faq.service.ts +++ b/src/main/webapp/app/faq/faq.service.ts @@ -118,22 +118,19 @@ export class FAQService { toggleFilter(category: string, activeFilters: Set) { if (activeFilters.has(category)) { activeFilters.delete(category); - return activeFilters; } else { activeFilters.add(category); - return activeFilters; } + return activeFilters; } applyFilters(activeFilters: Set, faqs: FAQ[]): FAQ[] { - let filteredFaq: FAQ[]; if (activeFilters.size === 0) { // If no filters selected, show all faqs - filteredFaq = faqs; + return faqs; } else { - filteredFaq = faqs.filter((faq) => this.hasFilteredCategory(faq, activeFilters)); + return faqs.filter((faq) => this.hasFilteredCategory(faq, activeFilters)); } - return filteredFaq; } hasFilteredCategory(faq: FAQ, filteredCategory: Set) { diff --git a/src/test/javascript/spec/component/faq/faq-update.component.spec.ts b/src/test/javascript/spec/component/faq/faq-update.component.spec.ts index ed0a8a293f1b..4693a72c65dd 100644 --- a/src/test/javascript/spec/component/faq/faq-update.component.spec.ts +++ b/src/test/javascript/spec/component/faq/faq-update.component.spec.ts @@ -11,7 +11,7 @@ import { MockTranslateService } from '../../helpers/mocks/service/mock-translate import { ArtemisTestModule } from '../../test.module'; import { FAQUpdateComponent } from 'app/faq/faq-update.component'; import { FAQService } from 'app/faq/faq.service'; -import { FAQ } from 'app/entities/faq.model'; +import { FAQ, FAQState } from 'app/entities/faq.model'; import { MonacoEditorComponent } from 'app/shared/monaco-editor/monaco-editor.component'; import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.module'; import { MockResizeObserver } from '../../helpers/mocks/service/mock-resize-observer'; @@ -91,7 +91,7 @@ describe('FaqUpdateComponent', () => { faqUpdateComponentFixture.detectChanges(); expect(createSpy).toHaveBeenCalledOnce(); - expect(createSpy).toHaveBeenCalledWith({ questionTitle: 'test1' }); + expect(createSpy).toHaveBeenCalledWith({ faqState: FAQState.ACCEPTED, questionTitle: 'test1' }); })); it('should edit a faq', fakeAsync(() => { diff --git a/src/test/javascript/spec/component/faq/faq.component.spec.ts b/src/test/javascript/spec/component/faq/faq.component.spec.ts index 7c8d5cd21f65..f2ca7cef71a3 100644 --- a/src/test/javascript/spec/component/faq/faq.component.spec.ts +++ b/src/test/javascript/spec/component/faq/faq.component.spec.ts @@ -116,7 +116,7 @@ describe('FaqComponent', () => { //is actually called when debugging, i dont get why it is 0. Need help expect(findAllSpy).toHaveBeenCalledOnce(); expect(findAllSpy).toHaveBeenCalledWith(1); - expect(faqComponent.faqs).toBeArrayOfSize(3); + expect(faqComponent.faqs).toHaveLength(3); }); it('should delete faq', () => { @@ -127,7 +127,7 @@ describe('FaqComponent', () => { expect(deleteSpy).toHaveBeenCalledOnce(); expect(deleteSpy).toHaveBeenCalledWith(faq1.id!); - expect(faqComponent.faqs).toBeArrayOfSize(2); + expect(faqComponent.faqs).toHaveLength(2); expect(faqComponent.faqs).not.toContain(faq1); expect(faqComponent.faqs).toEqual(faqComponent.filteredFaqs); }); From f33cad49be4e1e1fd808a741141fdb3ab5a2f489 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Mon, 23 Sep 2024 14:45:57 +0200 Subject: [PATCH 65/68] Fixed E2E test --- src/main/resources/config/liquibase/master.xml | 2 +- .../webapp/app/course/manage/course-update.component.ts | 2 +- src/main/webapp/app/faq/faq.service.ts | 6 +----- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index f7969f6b81bb..7921118cdfa3 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -22,8 +22,8 @@ - + diff --git a/src/main/webapp/app/course/manage/course-update.component.ts b/src/main/webapp/app/course/manage/course-update.component.ts index 66d951bc42bc..16f8ed1ab0e3 100644 --- a/src/main/webapp/app/course/manage/course-update.component.ts +++ b/src/main/webapp/app/course/manage/course-update.component.ts @@ -71,7 +71,7 @@ export class CourseUpdateComponent implements OnInit { faExclamationTriangle = faExclamationTriangle; faPen = faPen; - faqEnabled: boolean; + faqEnabled: boolean = true; //default value communicationEnabled = true; messagingEnabled = true; ltiEnabled = false; diff --git a/src/main/webapp/app/faq/faq.service.ts b/src/main/webapp/app/faq/faq.service.ts index 7e690ba0df01..2088129a1bdc 100644 --- a/src/main/webapp/app/faq/faq.service.ts +++ b/src/main/webapp/app/faq/faq.service.ts @@ -4,7 +4,6 @@ import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { FAQ, FAQState } from 'app/entities/faq.model'; import { FAQCategory } from 'app/entities/faq-category.model'; -import { AlertService } from 'app/core/util/alert.service'; type EntityResponseType = HttpResponse; type EntityArrayResponseType = HttpResponse; @@ -13,10 +12,7 @@ type EntityArrayResponseType = HttpResponse; export class FAQService { public resourceUrl = 'api/courses'; - constructor( - protected http: HttpClient, - protected alertService: AlertService, - ) {} + constructor(protected http: HttpClient) {} create(faq: FAQ): Observable { const copy = FAQService.convertFaqFromClient(faq); From e80e7fd72a2c50b0bf21ca43048e06928e78f95a Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Mon, 23 Sep 2024 17:29:26 +0200 Subject: [PATCH 66/68] Integrated changes, fixed last test --- .../communication/service/FaqService.java | 52 ------------------- .../communication/web/FaqResource.java | 27 +++++----- .../course/manage/course-update.component.ts | 2 +- src/main/webapp/app/faq/faq.service.ts | 4 +- .../course-faq-accordion-component.html | 2 +- .../de/tum/cit/aet/artemis/FaqFactory.java | 3 -- .../cit/aet/artemis/FaqIntegrationTest.java | 3 +- .../faq/faq-update.component.spec.ts | 3 -- .../spec/component/faq/faq.component.spec.ts | 25 ++++----- 9 files changed, 29 insertions(+), 92 deletions(-) delete mode 100644 src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java deleted file mode 100644 index 127fe3af84f6..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/FaqService.java +++ /dev/null @@ -1,52 +0,0 @@ -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 { - - 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 findAllCategoriesByCourseId(long courseId) { - return faqRepository.findAllCategoriesByCourseId(courseId); - } - - public Optional findById(Long faqId) { - return faqRepository.findById(faqId); - } - - public Set findAllByCourseId(Long courseId) { - return faqRepository.findAllByCourseId(courseId); - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java index 8da9e7d0f1c9..e0d884d2e304 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java @@ -21,10 +21,9 @@ 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.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.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; @@ -47,17 +46,17 @@ public class FaqResource { @Value("${jhipster.clientApp.name}") private String applicationName; - private final FaqService faqService; - private final CourseRepository courseRepository; + private final FaqRepository faqRepository; + private final AuthorizationCheckService authCheckService; - public FaqResource(FaqService faqService, CourseRepository courseRepository, AuthorizationCheckService authCheckService) { + public FaqResource(CourseRepository courseRepository, AuthorizationCheckService authCheckService, FaqRepository faqRepository) { - this.faqService = faqService; this.courseRepository = courseRepository; this.authCheckService = authCheckService; + this.faqRepository = faqRepository; } /** @@ -76,7 +75,7 @@ public ResponseEntity createFaq(@RequestBody Faq faq) throws URISyntaxExcep } authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); - Faq savedFaq = faqService.save(faq); + Faq savedFaq = faqRepository.save(faq); return ResponseEntity.created(new URI("/api/faqs/" + savedFaq.getId())).body(savedFaq); } @@ -96,8 +95,8 @@ public ResponseEntity updateFaq(@RequestBody Faq faq, @PathVariable Long fa throw new BadRequestAlertException("Id of FAQ and path must match", ENTITY_NAME, "idNull"); } authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); - Faq existingFaq = faqService.findById(faqId).orElseThrow(() -> new EntityNotFoundException("FAQ not found", faqId)); - Faq result = faqService.save(faq); + Faq existingFaq = faqRepository.findByIdElseThrow(faqId); + Faq result = faqRepository.save(faq); return ResponseEntity.ok().body(result); } @@ -111,7 +110,7 @@ public ResponseEntity updateFaq(@RequestBody Faq faq, @PathVariable Long fa @EnforceAtLeastStudent public ResponseEntity getFaq(@PathVariable Long faqId) { log.debug("REST request to get faq {}", faqId); - Faq faq = faqService.findById(faqId).orElseThrow(() -> new EntityNotFoundException("FAQ not found", faqId)); + Faq faq = faqRepository.findByIdElseThrow(faqId); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, faq.getCourse(), null); return ResponseEntity.ok(faq); } @@ -127,9 +126,9 @@ public ResponseEntity getFaq(@PathVariable Long faqId) { public ResponseEntity deleteFaq(@PathVariable Long faqId) { log.debug("REST request to delete faq {}", faqId); - Faq faq = faqService.findById(faqId).orElseThrow(() -> new EntityNotFoundException("FAQ not found", faqId)); + Faq faq = faqRepository.findByIdElseThrow(faqId); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); - faqService.deleteById(faqId); + faqRepository.deleteById(faqId); return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, faqId.toString())).build(); } @@ -146,7 +145,7 @@ public ResponseEntity> getFaqForCourse(@PathVariable Long courseId) { Course course = getCourseForRequest(courseId); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); - Set faqs = faqService.findAllByCourseId(courseId); + Set faqs = faqRepository.findAllByCourseId(courseId); return ResponseEntity.ok().body(faqs); } @@ -163,7 +162,7 @@ public ResponseEntity> getFaqCategoriesForCourse(@PathVariable Long Course course = getCourseForRequest(courseId); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); - Set faqs = faqService.findAllCategoriesByCourseId(courseId); + Set faqs = faqRepository.findAllCategoriesByCourseId(courseId); return ResponseEntity.ok().body(faqs); } diff --git a/src/main/webapp/app/course/manage/course-update.component.ts b/src/main/webapp/app/course/manage/course-update.component.ts index 16f8ed1ab0e3..f0dcaa47c9e8 100644 --- a/src/main/webapp/app/course/manage/course-update.component.ts +++ b/src/main/webapp/app/course/manage/course-update.component.ts @@ -71,7 +71,7 @@ export class CourseUpdateComponent implements OnInit { faExclamationTriangle = faExclamationTriangle; faPen = faPen; - faqEnabled: boolean = true; //default value + faqEnabled: true; //default value communicationEnabled = true; messagingEnabled = true; ltiEnabled = false; diff --git a/src/main/webapp/app/faq/faq.service.ts b/src/main/webapp/app/faq/faq.service.ts index 2088129a1bdc..059a10b95f99 100644 --- a/src/main/webapp/app/faq/faq.service.ts +++ b/src/main/webapp/app/faq/faq.service.ts @@ -16,7 +16,7 @@ export class FAQService { create(faq: FAQ): Observable { const copy = FAQService.convertFaqFromClient(faq); - faq.faqState = FAQState.ACCEPTED; + copy.faqState = FAQState.ACCEPTED; return this.http.post(`api/faqs`, copy, { observe: 'response' }).pipe( map((res: EntityResponseType) => { return res; @@ -59,7 +59,7 @@ export class FAQService { * @param res the response */ static convertFaqCategoriesFromServer(res: ERT): ERT { - if (res.body && res.body.categories) { + if (res.body?.categories) { FAQService.parseFaqCategories(res.body); } return res; diff --git a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html index 3e41b0eeefab..6f985dcfba2f 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html +++ b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html @@ -1,6 +1,6 @@
-

+

{{faq().questionTitle}}

@for (category of faq().categories; track category){ diff --git a/src/test/java/de/tum/cit/aet/artemis/FaqFactory.java b/src/test/java/de/tum/cit/aet/artemis/FaqFactory.java index 92b23c833cdb..ed782cb289ce 100644 --- a/src/test/java/de/tum/cit/aet/artemis/FaqFactory.java +++ b/src/test/java/de/tum/cit/aet/artemis/FaqFactory.java @@ -25,7 +25,4 @@ public static Set generateFaqCategories() { categories.add("this is also a category"); return categories; } - - private FaqFactory() { - } } diff --git a/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java index c56f34ad916d..45a9bfb47f05 100644 --- a/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java @@ -16,6 +16,7 @@ import de.tum.cit.aet.artemis.communication.domain.FaqState; 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.EntityNotFoundException; class FaqIntegrationTest extends AbstractSpringIntegrationIndependentTest { @@ -105,7 +106,7 @@ void updateFaq_correctRequestBody_shouldUpdateFaq() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetFaqCategoriesByCourseId() throws Exception { - Faq faq = faqRepository.findById(this.faq.getId()).orElseThrow(); + Faq faq = faqRepository.findById(this.faq.getId()).orElseThrow(EntityNotFoundException::new); Set categories = faq.getCategories(); Set returnedCategories = request.get("/api/courses/" + faq.getCourse().getId() + "/faq-categories", HttpStatus.OK, Set.class); assertThat(categories).isEqualTo(returnedCategories); diff --git a/src/test/javascript/spec/component/faq/faq-update.component.spec.ts b/src/test/javascript/spec/component/faq/faq-update.component.spec.ts index 4693a72c65dd..bc00d1df79bc 100644 --- a/src/test/javascript/spec/component/faq/faq-update.component.spec.ts +++ b/src/test/javascript/spec/component/faq/faq-update.component.spec.ts @@ -38,9 +38,6 @@ describe('FaqUpdateComponent', () => { parent: { data: of({ course: { id: 1 } }), }, - queryParams: of({ - params: {}, - }), snapshot: { paramMap: convertToParamMap({ courseId: '1', diff --git a/src/test/javascript/spec/component/faq/faq.component.spec.ts b/src/test/javascript/spec/component/faq/faq.component.spec.ts index f2ca7cef71a3..28214f64eb44 100644 --- a/src/test/javascript/spec/component/faq/faq.component.spec.ts +++ b/src/test/javascript/spec/component/faq/faq.component.spec.ts @@ -58,9 +58,6 @@ describe('FaqComponent', () => { parent: { data: of({ course: { id: 1 } }), }, - queryParams: of({ - params: {}, - }), snapshot: { paramMap: convertToParamMap({ courseId: '1', @@ -93,16 +90,17 @@ describe('FaqComponent', () => { }, }), ], - }).compileComponents(); + }) + .compileComponents() + .then(() => { + global.ResizeObserver = jest.fn().mockImplementation((callback: ResizeObserverCallback) => { + return new MockResizeObserver(callback); + }); + faqComponentFixture = TestBed.createComponent(FAQComponent); + faqComponent = faqComponentFixture.componentInstance; - global.ResizeObserver = jest.fn().mockImplementation((callback: ResizeObserverCallback) => { - return new MockResizeObserver(callback); - }); - faqComponentFixture = TestBed.createComponent(FAQComponent); - faqComponent = faqComponentFixture.componentInstance; - - faqService = TestBed.inject(FAQService); - faqComponentFixture.detectChanges(); + faqService = TestBed.inject(FAQService); + }); }); afterEach(() => { @@ -113,7 +111,6 @@ describe('FaqComponent', () => { const findAllSpy = jest.spyOn(faqService, 'findAllByCourseId'); faqComponentFixture.detectChanges(); - //is actually called when debugging, i dont get why it is 0. Need help expect(findAllSpy).toHaveBeenCalledOnce(); expect(findAllSpy).toHaveBeenCalledWith(1); expect(faqComponent.faqs).toHaveLength(3); @@ -121,10 +118,8 @@ describe('FaqComponent', () => { it('should delete faq', () => { const deleteSpy = jest.spyOn(faqService, 'delete'); - faqComponentFixture.detectChanges(); faqComponent.deleteFaq(faq1.id!); - expect(deleteSpy).toHaveBeenCalledOnce(); expect(deleteSpy).toHaveBeenCalledWith(faq1.id!); expect(faqComponent.faqs).toHaveLength(2); From 688305d14ae2e0acec9b563a04608b66ecf6dc14 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Mon, 23 Sep 2024 20:11:19 +0200 Subject: [PATCH 67/68] refixed e2e test --- src/main/webapp/app/course/manage/course-update.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/course/manage/course-update.component.ts b/src/main/webapp/app/course/manage/course-update.component.ts index f0dcaa47c9e8..624c44b77c60 100644 --- a/src/main/webapp/app/course/manage/course-update.component.ts +++ b/src/main/webapp/app/course/manage/course-update.component.ts @@ -71,7 +71,7 @@ export class CourseUpdateComponent implements OnInit { faExclamationTriangle = faExclamationTriangle; faPen = faPen; - faqEnabled: true; //default value + faqEnabled = true; //default value communicationEnabled = true; messagingEnabled = true; ltiEnabled = false; From 306592bb368f836e12773db1926ce2634d9a8bec Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 24 Sep 2024 13:00:02 +0200 Subject: [PATCH 68/68] fixed doc --- .../de/tum/cit/aet/artemis/communication/web/FaqResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java index e0d884d2e304..9c02a78078d1 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java @@ -150,7 +150,7 @@ public ResponseEntity> getFaqForCourse(@PathVariable Long courseId) { } /** - * 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 faqs in body