From b3bf2bb468a8c244d3a8504dc3bbb5996fcff4ac Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 3 Sep 2024 16:05:17 +0200 Subject: [PATCH 001/107] 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 002/107] 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 003/107] 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 004/107] 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 005/107] 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 006/107] 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 007/107] 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 008/107] 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 009/107] 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 010/107] 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 011/107] 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 012/107] 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 013/107] 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 014/107] 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 015/107] 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 016/107] 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 017/107] 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 018/107] 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 019/107] 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 020/107] 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 021/107] 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 022/107] 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 023/107] 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 024/107] 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 025/107] 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 026/107] 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 027/107] 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 028/107] 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 029/107] 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 030/107] 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 031/107] 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 032/107] 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 033/107] 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 034/107] 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 035/107] 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) { diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index 0ca4ed856d3c..ba4a647c65a5 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -85,6 +85,7 @@ export class FAQComponent implements OnInit, OnDestroy { private handleDeleteSuccess(faqId: number) { this.faqs = this.faqs.filter((faq) => faq.id !== faqId); this.dialogErrorSource.next(''); + this.loadCourseFaqCategories(this.courseId); this.applyFilters(); } diff --git a/src/main/webapp/app/overview/course-overview.component.ts b/src/main/webapp/app/overview/course-overview.component.ts index 9ec72eec1fdb..b6a27f2a43d4 100644 --- a/src/main/webapp/app/overview/course-overview.component.ts +++ b/src/main/webapp/app/overview/course-overview.component.ts @@ -337,11 +337,6 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit sidebarItems.push(faqItem); } - if (this.course?.learningPathsEnabled) { - const learningPathItem: SidebarItem = this.getLearningPathItems(); - sidebarItems.push(learningPathItem); - } - return sidebarItems; } From da5a0e04eca8f614d98e47836fff628cd616a03d Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 24 Sep 2024 18:06:34 +0200 Subject: [PATCH 072/107] added client tests --- .../course-faq/course-faq.component.html | 2 +- .../course-faq/course-faq.component.ts | 6 +- .../faq/faq-update.component.spec.ts | 15 +- .../spec/component/faq/faq.component.spec.ts | 10 ++ .../course-faq-accordion.component.spec.ts | 36 +++++ .../course-faq/course-faq.component.spec.ts | 129 ++++++++++++++++++ 6 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 src/test/javascript/spec/component/overview/course-faq/course-faq-accordion.component.spec.ts create mode 100644 src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts 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 8989aa5d897c..04d8167d53ba 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 @@ -26,7 +26,7 @@
- @for (faq of this.filteredFaq; track faq) { + @for (faq of this.filteredFaqs; track faq) {
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 eccc89768a1f..1d2f52505ce5 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 @@ -2,7 +2,6 @@ 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, faTimes } from '@fortawesome/free-solid-svg-icons'; import { ButtonType } from 'app/shared/components/button.component'; import { SidebarData } from 'app/types/sidebar'; @@ -23,7 +22,6 @@ import { onError } from 'app/shared/util/global.utils'; templateUrl: './course-faq.component.html', styleUrls: ['../course-overview.scss', './course-faq.component.scss', '../../faq/faq.component.scss'], encapsulation: ViewEncapsulation.None, - providers: [MetisService], standalone: true, imports: [ArtemisSharedComponentModule, ArtemisSharedModule, CourseFaqAccordionComponent, CustomExerciseCategoryBadgeComponent], }) @@ -34,7 +32,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy { courseId: number; faqs: FAQ[]; - filteredFaq: FAQ[]; + filteredFaqs: FAQ[]; existingCategories: FAQCategory[]; activeFilters = new Set(); @@ -98,6 +96,6 @@ export class CourseFaqComponent implements OnInit, OnDestroy { } private applyFilters(): void { - this.filteredFaq = this.faqService.applyFilters(this.activeFilters, this.faqs); + this.filteredFaqs = 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 bc00d1df79bc..54a48a3b7040 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 @@ -17,6 +17,7 @@ import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.modul import { MockResizeObserver } from '../../helpers/mocks/service/mock-resize-observer'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AlertService } from 'app/core/util/alert.service'; +import { FAQCategory } from 'app/entities/faq-category.model'; describe('FaqUpdateComponent', () => { let faqUpdateComponentFixture: ComponentFixture; @@ -102,7 +103,8 @@ describe('FaqUpdateComponent', () => { new HttpResponse({ body: { id: 6, - title: 'test1Updated', + questionTitle: 'test1Updated', + questionAnswer: 'answer', course: { id: 1, }, @@ -135,4 +137,15 @@ describe('FaqUpdateComponent', () => { const expectedPath = ['course-management', '1', 'faqs']; expect(navigateSpy).toHaveBeenCalledWith(expectedPath); })); + + it('should update categories', fakeAsync(() => { + const categories = [new FAQCategory('category1', 'red'), new FAQCategory('category2', 'blue')]; + + faqUpdateComponentFixture.detectChanges(); + + faqUpdateComponent.updateCategories(categories); + + expect(faqUpdateComponent.faqCategories).toEqual(categories); + expect(faqUpdateComponent.faq.categories).toEqual(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 28214f64eb44..bf5d23901b40 100644 --- a/src/test/javascript/spec/component/faq/faq.component.spec.ts +++ b/src/test/javascript/spec/component/faq/faq.component.spec.ts @@ -126,4 +126,14 @@ describe('FaqComponent', () => { expect(faqComponent.faqs).not.toContain(faq1); expect(faqComponent.faqs).toEqual(faqComponent.filteredFaqs); }); + + it('should toggle filter correctly', () => { + const toggleFilterSpy = jest.spyOn(faqService, 'toggleFilter'); + faqComponentFixture.detectChanges(); + faqComponent.toggleFilters('category2'); + expect(toggleFilterSpy).toHaveBeenCalledOnce(); + expect(faqComponent.filteredFaqs).toHaveLength(2); + expect(faqComponent.filteredFaqs).not.toContain(faq1); + expect(faqComponent.filteredFaqs).toEqual([faq2, faq3]); + }); }); diff --git a/src/test/javascript/spec/component/overview/course-faq/course-faq-accordion.component.spec.ts b/src/test/javascript/spec/component/overview/course-faq/course-faq-accordion.component.spec.ts new file mode 100644 index 000000000000..400c305f573a --- /dev/null +++ b/src/test/javascript/spec/component/overview/course-faq/course-faq-accordion.component.spec.ts @@ -0,0 +1,36 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent, MockDirective } from 'ng-mocks'; +import { CourseFaqAccordionComponent } from 'app/overview/course-faq/course-faq-accordion-component'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; +import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; +import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; +import { input } from '@angular/core'; + +describe('CourseFaqAccordionComponent', () => { + let courseFaqAccordionComponent: CourseFaqAccordionComponent; + let courseFaqAccordionComponentFixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisMarkdownModule], + declarations: [CourseFaqAccordionComponent, MockDirective(TranslateDirective), MockComponent(CustomExerciseCategoryBadgeComponent)], + }) + .compileComponents() + .then(() => { + courseFaqAccordionComponentFixture = TestBed.createComponent(CourseFaqAccordionComponent); + courseFaqAccordionComponent = courseFaqAccordionComponentFixture.componentInstance; + TestBed.runInInjectionContext(() => (courseFaqAccordionComponent.faq = input({ id: 1, questionTitle: 'Title?', questionAnswer: 'Answer', categories: [] }))); + }); + }); + + afterEach(() => { + courseFaqAccordionComponent.ngOnDestroy(); + courseFaqAccordionComponentFixture.destroy(); + }); + + it('should initialize', () => { + courseFaqAccordionComponentFixture.detectChanges(); + expect(courseFaqAccordionComponent).not.toBeNull(); + courseFaqAccordionComponent.ngOnDestroy(); + }); +}); diff --git a/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts new file mode 100644 index 000000000000..bdf4a67feb90 --- /dev/null +++ b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts @@ -0,0 +1,129 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { TranslateService } from '@ngx-translate/core'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks'; +import { of } from 'rxjs'; +import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; +import { HttpResponse } from '@angular/common/http'; +import { CourseFaqComponent } from 'app/overview/course-faq/course-faq.component'; +import { AlertService } from 'app/core/util/alert.service'; +import { FAQService } from 'app/faq/faq.service'; +import { MockRouter } from '../../../helpers/mocks/mock-router'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; +import { CourseFaqAccordionComponent } from 'app/overview/course-faq/course-faq-accordion-component'; +import { FAQ } from 'app/entities/faq.model'; +import { FAQCategory } from 'app/entities/faq-category.model'; + +describe('CourseFaqs', () => { + let courseFaqComponentFixture: ComponentFixture; + let courseFaqComponent: CourseFaqComponent; + + 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: [ArtemisSharedComponentModule, ArtemisSharedModule, MockComponent(CustomExerciseCategoryBadgeComponent), MockComponent(CourseFaqAccordionComponent)], + declarations: [CourseFaqComponent, MockPipe(ArtemisTranslatePipe), MockComponent(FaIconComponent), MockDirective(TranslateDirective)], + providers: [ + MockProvider(AlertService), + MockProvider(FAQService), + { provide: Router, useClass: MockRouter }, + { provide: TranslateService, useClass: MockTranslateService }, + { + provide: ActivatedRoute, + useValue: { + parent: { + params: of({ 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, + }), + ); + }, + applyFilters: () => { + return [faq2, faq3]; + }, + }), + ], + }) + .compileComponents() + .then(() => { + courseFaqComponentFixture = TestBed.createComponent(CourseFaqComponent); + courseFaqComponent = courseFaqComponentFixture.componentInstance; + + faqService = TestBed.inject(FAQService); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should initialize', () => { + courseFaqComponentFixture.detectChanges(); + expect(courseFaqComponent).not.toBeNull(); + courseFaqComponent.ngOnDestroy(); + }); + + it('should fetch faqs when initialized', () => { + const findAllSpy = jest.spyOn(faqService, 'findAllByCourseId'); + + courseFaqComponentFixture.detectChanges(); + expect(findAllSpy).toHaveBeenCalledOnce(); + expect(findAllSpy).toHaveBeenCalledWith(1); + expect(courseFaqComponent.faqs).toHaveLength(3); + }); + + it('should toggle filter correctly', () => { + const toggleFilterSpy = jest.spyOn(faqService, 'toggleFilter'); + courseFaqComponentFixture.detectChanges(); + courseFaqComponent.toggleFilters('category2'); + expect(toggleFilterSpy).toHaveBeenCalledOnce(); + expect(courseFaqComponent.filteredFaqs).toHaveLength(2); + expect(courseFaqComponent.filteredFaqs).not.toContain(faq1); + expect(courseFaqComponent.filteredFaqs).toEqual([faq2, faq3]); + }); +}); From 3f8e693fc060ba3a972e176f9e058c240144de62 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 24 Sep 2024 19:36:46 +0200 Subject: [PATCH 073/107] increased client tests coverage --- .../course-faq/course-faq.component.html | 2 +- .../faq/faq-update.component.spec.ts | 22 +++++++++++++++++-- 2 files changed, 21 insertions(+), 3 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 04d8167d53ba..6d8a9c4872a0 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 @@ -11,7 +11,7 @@
  • - @for (faq of filteredFaqs; track trackId(i, faq); let i = $index) { + @for (faq of filteredFaqs; track faq.id; 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 036/107] 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 037/107] 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 038/107] 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 039/107] 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 040/107] 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 041/107] 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 042/107] 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 043/107] 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 044/107] 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 049/107] 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 050/107] 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 051/107] 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 052/107] 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 053/107] 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 054/107] 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 055/107] 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 056/107] 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 057/107] 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 058/107] 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 059/107] 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 060/107] 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 061/107] 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 062/107] 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 063/107] 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 064/107] 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 065/107] 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 066/107] 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 067/107] 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 068/107] 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 From a627e433df0463dfff8ffc1a1fbcc7e87dff22ab Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 24 Sep 2024 13:36:20 +0200 Subject: [PATCH 069/107] use base class --- src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a8cb50eb4d83..fd87fbbb2b93 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; @@ -635,7 +635,7 @@ public void setEnrollmentEnabled(Boolean enrollmentEnabled) { this.enrollmentEnabled = enrollmentEnabled; } - public Boolean isFaqEnabled() { + public boolean isFaqEnabled() { return faqEnabled; } From 9b1ced2d92d1bb360df10746c7c87ecdaa6540bd Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 24 Sep 2024 14:04:50 +0200 Subject: [PATCH 070/107] use base class --- 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 fd87fbbb2b93..8000a24c0b55 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 @@ -639,7 +639,7 @@ public boolean isFaqEnabled() { return faqEnabled; } - public void setFaqEnabled(Boolean faqEnabled) { + public void setFaqEnabled(boolean faqEnabled) { this.faqEnabled = faqEnabled; } From eda1de3025ccd9858ec64d5134019330e26aeb8f Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 24 Sep 2024 14:36:07 +0200 Subject: [PATCH 071/107] adressed issues found in testing session --- src/main/webapp/app/faq/faq.component.html | 3 ++- src/main/webapp/app/faq/faq.component.ts | 1 + src/main/webapp/app/overview/course-overview.component.ts | 5 ----- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index 7b0f1312fc12..e13eb7a35d04 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -13,6 +13,7 @@

    [ngClass]="{ 'btn-secondary': activeFilters.size === 0, 'btn-success': activeFilters.size > 0 }" ngbDropdownToggle id="filter-dropdown-button" + [disabled]="!hasCategories" > @@ -55,7 +56,7 @@

    -

    + -
    +
    @for (category of faq.categories; track category) { } diff --git a/src/main/webapp/app/faq/faq.component.scss b/src/main/webapp/app/faq/faq.component.scss index 6cfed334a170..2f6a019961e3 100644 --- a/src/main/webapp/app/faq/faq.component.scss +++ b/src/main/webapp/app/faq/faq.component.scss @@ -2,7 +2,3 @@ margin-top: 10px; margin-left: 4px; } - -.category-container { - display: flex; -} 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 d0e3cba99120..a7948ad4f91c 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 @@ -7,8 +7,8 @@ import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; @Component({ selector: 'jhi-course-faq-accordion', - templateUrl: './course-faq-accordion-component.html', - styleUrl: './course-faq-accordion-component.scss', + templateUrl: './course-faq-accordion.component.html', + styleUrl: './course-faq-accordion.component.scss', standalone: true, imports: [TranslateDirective, CustomExerciseCategoryBadgeComponent, ArtemisMarkdownModule], 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 similarity index 50% rename from src/main/webapp/app/overview/course-faq/course-faq-accordion-component.html rename to src/main/webapp/app/overview/course-faq/course-faq-accordion.component.html index 6f985dcfba2f..b3f278459857 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,10 +1,10 @@
    -

    {{faq().questionTitle}}

    +

    {{ faq().questionTitle }}

    - @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-accordion-component.scss b/src/main/webapp/app/overview/course-faq/course-faq-accordion.component.scss similarity index 100% rename from src/main/webapp/app/overview/course-faq/course-faq-accordion-component.scss rename to src/main/webapp/app/overview/course-faq/course-faq-accordion.component.scss 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 fbe3829a101a..fa1576966be7 100644 --- a/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java @@ -137,14 +137,14 @@ void testGetFaqByFaqId() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testGetFaqByFaqId_shouldNotGet_IdMissmatch() throws Exception { + void testGetFaqByFaqId_shouldNotGet_IdMismatch() throws Exception { Faq faq = faqRepository.findById(this.faq.getId()).orElseThrow(EntityNotFoundException::new); Faq returnedFaq = request.get("/api/courses/" + faq.getCourse().getId() + 1 + "/faqs/" + faq.getId(), HttpStatus.BAD_REQUEST, Faq.class); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void deleteFAQ_shouldDeleteFAQ() throws Exception { + void deleteFaq_shouldDeleteFAQ() throws Exception { Faq faq = faqRepository.findById(this.faq.getId()).orElseThrow(EntityNotFoundException::new); request.delete("/api/courses/" + faq.getCourse().getId() + "/faqs/" + faq.getId(), HttpStatus.OK); Optional faqOptional = faqRepository.findById(faq.getId()); @@ -153,7 +153,7 @@ void deleteFAQ_shouldDeleteFAQ() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void deleteFAQ_IdsDoNotMatch_shouldNotDeleteFAQ() throws Exception { + void deleteFaq_IdsDoNotMatch_shouldNotDeleteFAQ() throws Exception { Faq faq = faqRepository.findById(this.faq.getId()).orElseThrow(EntityNotFoundException::new); request.delete("/api/courses/" + faq.getCourse().getId() + 1 + "/faqs/" + faq.getId(), HttpStatus.BAD_REQUEST); Optional faqOptional = faqRepository.findById(faq.getId()); 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 b682fd603467..b0d86199e0d0 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 @@ -117,8 +117,7 @@ describe('FaqUpdateComponent', () => { faqUpdateComponent.save(); tick(); - expect(createSpy).toHaveBeenCalledOnce(); - expect(createSpy).toHaveBeenCalledWith(courseId, { faqState: FAQState.ACCEPTED, questionTitle: 'test1' }); + expect(createSpy).toHaveBeenCalledExactlyOnceWith(courseId, { faqState: FAQState.ACCEPTED, questionTitle: 'test1' }); expect(faqUpdateComponent.isSaving).toBeFalse(); })); @@ -164,7 +163,7 @@ describe('FaqUpdateComponent', () => { tick(); expect(previousState).toHaveBeenCalledOnce(); - const expectedPath = ['course-management', '1', 'faqs']; + 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 index bebf904cb0ae..3b6af3b1253f 100644 --- a/src/test/javascript/spec/component/faq/faq.component.spec.ts +++ b/src/test/javascript/spec/component/faq/faq.component.spec.ts @@ -20,6 +20,15 @@ import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-catego import { AlertService } from 'app/core/util/alert.service'; import { SortService } from 'app/shared/service/sort.service'; +function createFaq(id: number, category: string, color: string): FAQ { + const faq = new FAQ(); + faq.id = id; + faq.questionTitle = 'questionTitle'; + faq.questionAnswer = 'questionAnswer'; + faq.categories = [new FAQCategory(category, color)]; + return faq; +} + describe('FaqComponent', () => { let faqComponentFixture: ComponentFixture; let faqComponent: FAQComponent; @@ -36,27 +45,13 @@ describe('FaqComponent', () => { let courseId: number; 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')]; + // In beforeEach: + faq1 = createFaq(1, 'category1', '#94a11c'); + faq2 = createFaq(2, 'category2', '#0ab84f'); + faq3 = createFaq(3, 'category3', '#0ab84f'); courseId = 1; - courseId = 1; TestBed.configureTestingModule({ imports: [ArtemisTestModule, ArtemisMarkdownEditorModule, MockModule(BrowserAnimationsModule)], declarations: [FAQComponent, MockRouterLinkDirective, MockComponent(CustomExerciseCategoryBadgeComponent)], @@ -126,6 +121,7 @@ describe('FaqComponent', () => { faqComponentFixture.detectChanges(); expect(findAllSpy).toHaveBeenCalledExactlyOnceWith(1); expect(faqComponent.faqs).toHaveLength(3); + expect(faqComponent.faqs).toEqual([faq1, faq2, faq3]); }); it('should catch error if loading fails', () => { @@ -139,7 +135,7 @@ describe('FaqComponent', () => { it('should delete faq', () => { const deleteSpy = jest.spyOn(faqService, 'delete'); faqComponentFixture.detectChanges(); - faqComponent.deleteFaq(1, faq1.id!); + faqComponent.deleteFaq(courseId, faq1.id!); expect(deleteSpy).toHaveBeenCalledOnce(); expect(deleteSpy).toHaveBeenCalledWith(courseId, faq1.id!); expect(faqComponent.faqs).toHaveLength(2); diff --git a/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts index 2d255a06fd7c..8873f453dce9 100644 --- a/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts @@ -19,6 +19,15 @@ import { CourseFaqAccordionComponent } from 'app/overview/course-faq/course-faq- import { FAQ } from 'app/entities/faq.model'; import { FAQCategory } from 'app/entities/faq-category.model'; +function createFaq(id: number, category: string, color: string): FAQ { + const faq = new FAQ(); + faq.id = id; + faq.questionTitle = 'questionTitle'; + faq.questionAnswer = 'questionAnswer'; + faq.categories = [new FAQCategory(category, color)]; + return faq; +} + describe('CourseFaqs', () => { let courseFaqComponentFixture: ComponentFixture; let courseFaqComponent: CourseFaqComponent; @@ -32,23 +41,11 @@ describe('CourseFaqs', () => { 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')]; + // In beforeEach: + faq1 = createFaq(1, 'category1', '#94a11c'); + faq2 = createFaq(2, 'category2', '#0ab84f'); + faq3 = createFaq(3, 'category3', '#0ab84f'); - faq3 = new FAQ(); - faq3.id = 3; - faq3.questionTitle = 'questionTitle'; - faq3.questionAnswer = 'questionAnswer'; - faq3.categories = [new FAQCategory('category3', '#0ab84f')]; TestBed.configureTestingModule({ imports: [ArtemisSharedComponentModule, ArtemisSharedModule, MockComponent(CustomExerciseCategoryBadgeComponent), MockComponent(CourseFaqAccordionComponent)], declarations: [CourseFaqComponent, MockPipe(ArtemisTranslatePipe), MockComponent(FaIconComponent), MockDirective(TranslateDirective)], diff --git a/src/test/javascript/spec/service/faq.service.spec.ts b/src/test/javascript/spec/service/faq.service.spec.ts index bbcc654f01b7..272e972d8542 100644 --- a/src/test/javascript/spec/service/faq.service.spec.ts +++ b/src/test/javascript/spec/service/faq.service.spec.ts @@ -112,7 +112,7 @@ describe('Faq Service', () => { expect(expectedResult.body).toEqual(expected); }); - it('should find faqs by courseId', async () => { + it('should find faqs by courseId', () => { const category = { color: '#6ae8ac', category: 'category1', @@ -132,7 +132,7 @@ describe('Faq Service', () => { expect(expectedResult.body).toEqual(expected); }); - it('should find all categories by courseId', async () => { + it('should find all categories by courseId', () => { const category = { color: '#6ae8ac', category: 'category1', @@ -152,7 +152,7 @@ describe('Faq Service', () => { expect(expectedResult.body).toEqual(expected); }); - it('should set add active filter correctly', async () => { + it('should set add active filter correctly', () => { let activeFilters = new Set(); activeFilters = service.toggleFilter('category1', activeFilters); @@ -160,7 +160,7 @@ describe('Faq Service', () => { expect(activeFilters.size).toBe(1); }); - it('should remove active filter correctly', async () => { + it('should remove active filter correctly', () => { let activeFilters = new Set(); activeFilters.add('category1'); activeFilters = service.toggleFilter('category1', activeFilters); @@ -169,7 +169,7 @@ describe('Faq Service', () => { expect(activeFilters.size).toBe(0); }); - it('should apply faqFilter correctly', async () => { + it('should apply faqFilter correctly', () => { const activeFilters = new Set(); const faq1 = new FAQ(); diff --git a/src/test/playwright/e2e/course/CourseManagement.spec.ts b/src/test/playwright/e2e/course/CourseManagement.spec.ts index dc79eca9b785..5c011a02b499 100644 --- a/src/test/playwright/e2e/course/CourseManagement.spec.ts +++ b/src/test/playwright/e2e/course/CourseManagement.spec.ts @@ -101,7 +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.setEnableFaq(courseData.enableFaqs); await courseCreation.setMaxComplaints(courseData.maxComplaints); await courseCreation.setMaxTeamComplaints(courseData.maxTeamComplaints); await courseCreation.setMaxComplaintsTimeDays(courseData.maxComplaintTimeDays); diff --git a/src/test/playwright/support/pageobjects/course/CourseCreationPage.ts b/src/test/playwright/support/pageobjects/course/CourseCreationPage.ts index 4255bde97814..cc582aadd368 100644 --- a/src/test/playwright/support/pageobjects/course/CourseCreationPage.ts +++ b/src/test/playwright/support/pageobjects/course/CourseCreationPage.ts @@ -148,10 +148,10 @@ export class CourseCreationPage { } /** - * Sets if complaints are enabled - * @param enableFaq if complaints should be enabled + * Sets if FAQs are enabled for the course + * @param enableFaq if FAQs should be enabled */ - async setEnableFaqs(enableFaq: boolean) { + async setEnableFaq(enableFaq: boolean) { const selector = this.page.locator('#field_faq_enabled'); if (enableFaq) { await selector.check(); From aff2daf45dc0dc9355b3a79fa81f243ada7a0dfe Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Fri, 27 Sep 2024 18:35:33 +0200 Subject: [PATCH 086/107] Adjusted css to use bootstrap --- src/main/webapp/app/faq/faq.component.html | 2 +- .../overview/course-faq/course-faq.component.html | 2 +- .../overview/course-faq/course-faq.component.scss | 13 ------------- .../app/overview/course-faq/course-faq.component.ts | 2 +- 4 files changed, 3 insertions(+), 16 deletions(-) delete mode 100644 src/main/webapp/app/overview/course-faq/course-faq.component.scss diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index b620fff4dbbd..1dac51a60117 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -30,7 +30,7 @@

    [checked]="activeFilters.has(category.category!)" type="checkbox" /> - + } 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 6d8a9c4872a0..ff249d442a9f 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 @@ -17,7 +17,7 @@ [checked]="activeFilters.has(category.category!)" type="checkbox" /> - + } 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 deleted file mode 100644 index 25093ce4e1ff..000000000000 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.scss +++ /dev/null @@ -1,13 +0,0 @@ -.second-layer-modal-bg { - background-color: var(--secondary); -} - -.module-bg { - background-color: var(--module-bg); -} - -.scroll-container { - max-height: 80vh; - overflow-y: auto; - overflow-x: hidden; -} 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 1d2f52505ce5..33939ef8c39f 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 @@ -20,7 +20,7 @@ import { onError } from 'app/shared/util/global.utils'; @Component({ selector: 'jhi-course-faq', templateUrl: './course-faq.component.html', - styleUrls: ['../course-overview.scss', './course-faq.component.scss', '../../faq/faq.component.scss'], + styleUrls: ['../course-overview.scss', '../../faq/faq.component.scss'], encapsulation: ViewEncapsulation.None, standalone: true, imports: [ArtemisSharedComponentModule, ArtemisSharedModule, CourseFaqAccordionComponent, CustomExerciseCategoryBadgeComponent], From 515b5863cd31c970c3d7a617b50cb950cee147c3 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Fri, 27 Sep 2024 18:41:25 +0200 Subject: [PATCH 087/107] Scrollbar --- src/main/webapp/app/faq/faq.component.scss | 4 ---- src/main/webapp/app/faq/faq.component.ts | 2 +- .../overview/course-faq/course-faq.component.scss | 13 +++++++++++++ .../app/overview/course-faq/course-faq.component.ts | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) delete mode 100644 src/main/webapp/app/faq/faq.component.scss create mode 100644 src/main/webapp/app/overview/course-faq/course-faq.component.scss diff --git a/src/main/webapp/app/faq/faq.component.scss b/src/main/webapp/app/faq/faq.component.scss deleted file mode 100644 index 2f6a019961e3..000000000000 --- a/src/main/webapp/app/faq/faq.component.scss +++ /dev/null @@ -1,4 +0,0 @@ -.category-badge { - margin-top: 10px; - margin-left: 4px; -} diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index 8425fc1cc27f..5d5c576489ce 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -19,7 +19,7 @@ import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; @Component({ selector: 'jhi-faq', templateUrl: './faq.component.html', - styleUrls: ['./faq.component.scss'], + styleUrls: [], standalone: true, imports: [ArtemisSharedModule, CustomExerciseCategoryBadgeComponent, ArtemisSharedComponentModule, ArtemisMarkdownModule], }) 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..9a444afe91a8 --- /dev/null +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.scss @@ -0,0 +1,13 @@ +.scroll-container { + max-height: 83vh; + overflow-y: auto; + overflow-x: hidden; +} + +.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 index 33939ef8c39f..dc80ffba9149 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 @@ -20,7 +20,7 @@ import { onError } from 'app/shared/util/global.utils'; @Component({ selector: 'jhi-course-faq', templateUrl: './course-faq.component.html', - styleUrls: ['../course-overview.scss', '../../faq/faq.component.scss'], + styleUrls: ['../course-overview.scss', 'course-faq.component.scss'], encapsulation: ViewEncapsulation.None, standalone: true, imports: [ArtemisSharedComponentModule, ArtemisSharedModule, CourseFaqAccordionComponent, CustomExerciseCategoryBadgeComponent], From 091c654cdf0e1ef1c8c232d15c057ce320a8f817 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Fri, 27 Sep 2024 18:46:19 +0200 Subject: [PATCH 088/107] inject --- .../app/overview/course-faq/course-faq.component.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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 dc80ffba9149..74910fccf5ef 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,4 +1,4 @@ -import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewEncapsulation, inject } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { map } from 'rxjs/operators'; import { Subject, Subscription } from 'rxjs'; @@ -49,12 +49,10 @@ export class CourseFaqComponent implements OnInit, OnDestroy { faTimes = faTimes; faFilter = faFilter; - constructor( - private route: ActivatedRoute, - private router: Router, - private faqService: FAQService, - private alertService: AlertService, - ) {} + private route = inject(ActivatedRoute); + private router = inject(Router); + private faqService = inject(FAQService); + private alertService = inject(AlertService); ngOnInit(): void { this.parentParamSubscription = this.route.parent!.params.subscribe((params) => { From 9627086cb5179ccff8c799f1a0e96c51a7c29d90 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Fri, 27 Sep 2024 18:53:55 +0200 Subject: [PATCH 089/107] fixed two test issues --- src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java | 1 + src/test/javascript/spec/service/faq.service.spec.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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 fa1576966be7..1e04f359c7dd 100644 --- a/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java @@ -157,6 +157,7 @@ void deleteFaq_IdsDoNotMatch_shouldNotDeleteFAQ() throws Exception { Faq faq = faqRepository.findById(this.faq.getId()).orElseThrow(EntityNotFoundException::new); request.delete("/api/courses/" + faq.getCourse().getId() + 1 + "/faqs/" + faq.getId(), HttpStatus.BAD_REQUEST); Optional faqOptional = faqRepository.findById(faq.getId()); + assertThat(faqOptional).isPresent(); } } diff --git a/src/test/javascript/spec/service/faq.service.spec.ts b/src/test/javascript/spec/service/faq.service.spec.ts index 272e972d8542..da324b38c1bf 100644 --- a/src/test/javascript/spec/service/faq.service.spec.ts +++ b/src/test/javascript/spec/service/faq.service.spec.ts @@ -199,7 +199,7 @@ describe('Faq Service', () => { expect(convertedCategory[0].color).toBe('red'); }); - it('should convert FAQ categories into strings', async () => { + it('should convert FAQ categories into strings', () => { const faq2 = new FAQ(); faq2.categories = [new FAQCategory('testing', 'red')]; const convertedCategory = FAQService.stringifyFaqCategories(faq2); From a38f268d7415fcf792d42783d2c9eb6733fd883b Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Fri, 27 Sep 2024 19:07:00 +0200 Subject: [PATCH 090/107] inserted onChange --- src/main/webapp/app/faq/faq-update.component.html | 12 +++++++++--- src/main/webapp/app/faq/faq-update.component.ts | 7 +++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/app/faq/faq-update.component.html b/src/main/webapp/app/faq/faq-update.component.html index f53c037fe670..ac85daeddfd6 100644 --- a/src/main/webapp/app/faq/faq-update.component.html +++ b/src/main/webapp/app/faq/faq-update.component.html @@ -12,12 +12,18 @@

    - +

    - +
    @@ -36,7 +42,7 @@

      -

    diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index ec7f3a5a9bbe..54896a9d520a 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -28,6 +28,7 @@ import { Course } from 'app/entities/course.model'; export class FAQUpdateComponent implements OnInit { faq: FAQ; isSaving: boolean; + isAllowedToSave: boolean; existingCategories: FAQCategory[]; faqCategories: FAQCategory[]; courseId: number; @@ -62,6 +63,7 @@ export class FAQUpdateComponent implements OnInit { } this.faqCategories = faq?.categories ? faq.categories : []; }); + this.canSave(); } /** @@ -146,8 +148,9 @@ export class FAQUpdateComponent implements OnInit { canSave() { if (this.faq.questionTitle && this.faq.questionAnswer) { - return this.faq.questionTitle?.trim().length > 0 && this.faq.questionAnswer?.trim().length > 0; + this.isAllowedToSave = this.faq.questionTitle?.trim().length > 0 && this.faq.questionAnswer?.trim().length > 0; + } else { + this.isAllowedToSave = false; } - return false; } } From b2e464749f9ace47cce7c499474b16f136024d9d Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Fri, 27 Sep 2024 19:25:06 +0200 Subject: [PATCH 091/107] fixed test --- .../spec/component/faq/faq-update.component.spec.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 b0d86199e0d0..98ec83cb8340 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 @@ -178,11 +178,14 @@ describe('FaqUpdateComponent', () => { it('should not be able to save unless title and question are filled', fakeAsync(() => { faqUpdateComponentFixture.detectChanges(); faqUpdateComponent.faq = { questionTitle: 'test1' } as FAQ; - expect(faqUpdateComponent.canSave()).toBeFalse(); + faqUpdateComponent.canSave(); + expect(faqUpdateComponent.isAllowedToSave).toBeFalse(); faqUpdateComponent.faq = { questionAnswer: 'test1' } as FAQ; - expect(faqUpdateComponent.canSave()).toBeFalse(); + faqUpdateComponent.canSave(); + expect(faqUpdateComponent.isAllowedToSave).toBeFalse(); faqUpdateComponent.faq = { questionTitle: 'test', questionAnswer: 'test1' } as FAQ; - expect(faqUpdateComponent.canSave()).toBeTrue(); + faqUpdateComponent.canSave(); + expect(faqUpdateComponent.isAllowedToSave).toBeTrue(); })); it('should fail while saving with ErrorResponse', fakeAsync(() => { From d7da76f313a81405ad0f5195bb28ca5d6293a75a Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Fri, 27 Sep 2024 19:38:31 +0200 Subject: [PATCH 092/107] fixed some coderabit hints --- .../de/tum/cit/aet/artemis/communication/web/FaqResource.java | 2 +- src/test/javascript/spec/component/faq/faq.component.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 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 077b07f9446c..7eaa82e19a9a 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 @@ -84,7 +84,7 @@ public ResponseEntity createFaq(@RequestBody Faq faq, @PathVariable Long Faq savedFaq = faqRepository.save(faq); FaqDTO dto = new FaqDTO(savedFaq); - return ResponseEntity.created(new URI("/api/faqs/" + courseId)).body(dto); + return ResponseEntity.created(new URI("/api/courses/" + courseId + "/faqs/" + savedFaq.getId())).body(dto); } /** 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 3b6af3b1253f..613cbcf8f58f 100644 --- a/src/test/javascript/spec/component/faq/faq.component.spec.ts +++ b/src/test/javascript/spec/component/faq/faq.component.spec.ts @@ -147,7 +147,7 @@ describe('FaqComponent', () => { const error = { status: 404 }; const deleteSpy = jest.spyOn(faqService, 'delete').mockReturnValue(throwError(() => new HttpErrorResponse(error))); faqComponentFixture.detectChanges(); - faqComponent.deleteFaq(1, faq1.id!); + faqComponent.deleteFaq(courseId, faq1.id!); expect(deleteSpy).toHaveBeenCalledOnce(); expect(deleteSpy).toHaveBeenCalledWith(courseId, faq1.id!); expect(faqComponent.faqs).toHaveLength(3); From ad5707b14737ad83c832815b6a39b0fb6316fd7a Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Fri, 27 Sep 2024 19:52:48 +0200 Subject: [PATCH 093/107] fixed some coderabit hints --- src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1e04f359c7dd..2f671040dd29 100644 --- a/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java @@ -80,14 +80,14 @@ void createFaq_correctRequestBody_shouldCreateFaq() throws Exception { void createFaq_alreadyId_shouldReturnBadRequest() throws Exception { Faq newFaq = FaqFactory.generateFaq(course1, FaqState.ACCEPTED, "title", "answer"); faq.setId(this.faq.getId()); - request.postWithResponseBody("/api/courses/" + course1.getId() + "/faqs", faq, Faq.class, HttpStatus.BAD_REQUEST); + request.postWithResponseBody("/api/courses/" + course1.getId() + "/faqs", newFaq, Faq.class, HttpStatus.BAD_REQUEST); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createFaq_courseId_noMatch_shouldReturnBadRequest() throws Exception { Faq newFaq = FaqFactory.generateFaq(course1, FaqState.ACCEPTED, "title", "answer"); - request.postWithResponseBody("/api/courses/" + course1.getId() + 1 + "/faqs", faq, Faq.class, HttpStatus.BAD_REQUEST); + request.postWithResponseBody("/api/courses/" + course1.getId() + 1 + "/faqs", newFaq, Faq.class, HttpStatus.BAD_REQUEST); } @Test From 0b54e6b4d1d0943a8368cea5821332dda2f69bda Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Sun, 29 Sep 2024 10:42:07 +0200 Subject: [PATCH 094/107] Adressed patricks comments, fixed failing test --- .../communication/web/FaqResource.java | 24 ++++---- .../course/manage/course-management.route.ts | 14 ++--- .../course/manage/course-update.component.ts | 1 + .../webapp/app/entities/faq-category.model.ts | 8 +-- src/main/webapp/app/entities/faq.model.ts | 10 ++-- .../webapp/app/faq/faq-update.component.html | 4 +- .../webapp/app/faq/faq-update.component.ts | 35 ++++++----- src/main/webapp/app/faq/faq.component.html | 2 +- src/main/webapp/app/faq/faq.component.ts | 43 +++++++------ src/main/webapp/app/faq/faq.routes.ts | 16 ++--- src/main/webapp/app/faq/faq.service.ts | 60 +++++++++---------- src/main/webapp/app/faq/faq.utils.ts | 6 +- .../course-faq-accordion-component.ts | 4 +- .../course-faq/course-faq.component.ts | 28 ++++----- .../category-selector.component.ts | 6 +- ...ustom-exercise-category-badge.component.ts | 4 +- .../webapp/i18n/de/student-dashboard.json | 3 +- .../webapp/i18n/en/student-dashboard.json | 1 + .../{ => communication}/FaqFactory.java | 2 +- .../FaqIntegrationTest.java | 5 +- .../faq/faq-update.component.spec.ts | 58 +++++++++--------- .../spec/component/faq/faq.component.spec.ts | 46 +++++++------- .../course-faq/course-faq.component.spec.ts | 26 ++++---- .../spec/service/faq.service.spec.ts | 46 +++++++------- 24 files changed, 223 insertions(+), 229 deletions(-) rename src/test/java/de/tum/cit/aet/artemis/{ => communication}/FaqFactory.java (94%) rename src/test/java/de/tum/cit/aet/artemis/{ => communication}/FaqIntegrationTest.java (97%) 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 7eaa82e19a9a..7cd7acc93551 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 @@ -51,10 +51,10 @@ public class FaqResource { private final CourseRepository courseRepository; - private final FaqRepository faqRepository; - private final AuthorizationCheckService authCheckService; + private final FaqRepository faqRepository; + public FaqResource(CourseRepository courseRepository, AuthorizationCheckService authCheckService, FaqRepository faqRepository) { this.courseRepository = courseRepository; @@ -63,10 +63,11 @@ public FaqResource(CourseRepository courseRepository, AuthorizationCheckService } /** - * POST /faqs : Create a new faq. + * POST /courses/:courseId/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 + * or with status 500 if the faq course id does not match with the path variable * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("courses/{courseId}/faqs") @@ -88,10 +89,11 @@ public ResponseEntity createFaq(@RequestBody Faq faq, @PathVariable Long } /** - * PUT /faqs/{faqId} : Updates an existing faq. + * PUT /courses/:courseId/faqs/{faqId} : Updates an existing faq. * - * @param faq the faq to update - * @param faqId id of the faq to be updated + * @param faq the faq to update + * @param faqId id of the faq to be updated * + * @param courseId the id of the course the faq belongs to * @return the ResponseEntity with status 200 (OK) and with body the updated faq, or with status 400 (Bad Request) if the faq is not valid, or with status 500 (Internal * Server Error) if the faq couldn't be updated */ @@ -113,9 +115,10 @@ public ResponseEntity updateFaq(@RequestBody Faq faq, @PathVariable Long } /** - * GET /faqs/:faqId : get the faq with the id faqId. + * GET /courses/:courseId/faqs/:faqId : get the faq with the id faqId. * - * @param faqId the faqId of the faq to retrieve + * @param faqId the faqId of the faq to retrieve * + * @param courseId the id of the course the faq belongs to * @return the ResponseEntity with status 200 (OK) and with body the faq, or with status 404 (Not Found) */ @GetMapping("courses/{courseId}/faqs/{faqId}") @@ -132,9 +135,10 @@ public ResponseEntity getFaq(@PathVariable Long faqId, @PathVariable Lon } /** - * DELETE /faqs/:faqId : delete the "id" faq. + * DELETE /courses/:courseId/faqs/:faqId : delete the "id" faq. * - * @param faqId the id of the faq to delete + * @param faqId the id of the faq to delete + * @param courseId the id of the course the faq belongs to * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("courses/{courseId}/faqs/{faqId}") diff --git a/src/main/webapp/app/course/manage/course-management.route.ts b/src/main/webapp/app/course/manage/course-management.route.ts index 809814ac5a22..ad4507123d1a 100644 --- a/src/main/webapp/app/course/manage/course-management.route.ts +++ b/src/main/webapp/app/course/manage/course-management.route.ts @@ -33,9 +33,9 @@ import { ImportPrerequisitesComponent } from 'app/course/competencies/import/imp import { CreatePrerequisiteComponent } from 'app/course/competencies/create/create-prerequisite.component'; import { EditPrerequisiteComponent } from 'app/course/competencies/edit/edit-prerequisite.component'; import { CourseImportStandardizedPrerequisitesComponent } from 'app/course/competencies/import-standardized-competencies/course-import-standardized-prerequisites.component'; -import { FAQComponent } from 'app/faq/faq.component'; -import { FAQUpdateComponent } from 'app/faq/faq-update.component'; -import { FAQResolve } from 'app/faq/faq.routes'; +import { FaqComponent } from 'app/faq/faq.component'; +import { FaqUpdateComponent } from 'app/faq/faq-update.component'; +import { FaqResolve } from 'app/faq/faq.routes'; export const courseManagementState: Routes = [ { @@ -342,7 +342,7 @@ export const courseManagementState: Routes = [ children: [ { path: '', - component: FAQComponent, + component: FaqComponent, resolve: { course: CourseManagementResolve, }, @@ -361,7 +361,7 @@ export const courseManagementState: Routes = [ children: [ { path: 'new', - component: FAQUpdateComponent, + component: FaqUpdateComponent, data: { authorities: [Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], pageTitle: 'global.generic.create', @@ -371,12 +371,12 @@ export const courseManagementState: Routes = [ { path: ':faqId', resolve: { - faq: FAQResolve, + faq: FaqResolve, }, children: [ { path: 'edit', - component: FAQUpdateComponent, + component: FaqUpdateComponent, data: { authorities: [Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], pageTitle: 'global.generic.edit', 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 19387d179a08..44552b2c338b 100644 --- a/src/main/webapp/app/course/manage/course-update.component.ts +++ b/src/main/webapp/app/course/manage/course-update.component.ts @@ -507,6 +507,7 @@ export class CourseUpdateComponent implements OnInit { this.courseForm.controls['editorGroupName'].setValue(editorGroupName); this.courseForm.controls['instructorGroupName'].setValue(instructorGroupName); } + changeFaqEnabled() { this.faqEnabled = !this.faqEnabled; this.courseForm.controls['faqEnabled'].setValue(this.faqEnabled); diff --git a/src/main/webapp/app/entities/faq-category.model.ts b/src/main/webapp/app/entities/faq-category.model.ts index 46ede47530db..4094ed34dc57 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,15 +8,15 @@ export class FAQCategory { this.category = category; } - equals(otherFaqCategory: FAQCategory): boolean { + equals(otherFaqCategory: FaqCategory): boolean { return this.color === otherFaqCategory.color && this.category === otherFaqCategory.category; } /** * @param otherFaqCategory - * @returns the alphanumerical order of the two exercise categories based on their display text + * @returns the alphanumerical order of the two categories based on their display text */ - compare(otherFaqCategory: FAQCategory): number { + compare(otherFaqCategory: FaqCategory): number { if (this.category === otherFaqCategory.category) { return 0; } diff --git a/src/main/webapp/app/entities/faq.model.ts b/src/main/webapp/app/entities/faq.model.ts index 35736ba9296d..ea6de090b2b2 100644 --- a/src/main/webapp/app/entities/faq.model.ts +++ b/src/main/webapp/app/entities/faq.model.ts @@ -1,18 +1,18 @@ 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 { +export enum FaqState { ACCEPTED, REJECTED, PROPOSED, } -export class FAQ implements BaseEntity { +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[]; + public categories?: FaqCategory[]; } diff --git a/src/main/webapp/app/faq/faq-update.component.html b/src/main/webapp/app/faq/faq-update.component.html index ac85daeddfd6..c812877b6637 100644 --- a/src/main/webapp/app/faq/faq-update.component.html +++ b/src/main/webapp/app/faq/faq-update.component.html @@ -12,7 +12,7 @@

    - +

    @@ -22,7 +22,7 @@

    diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index 54896a9d520a..5e5c4aae5103 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -7,10 +7,10 @@ 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, FAQState } from 'app/entities/faq.model'; -import { FAQService } from 'app/faq/faq.service'; +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'; +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'; @@ -25,14 +25,13 @@ import { Course } from 'app/entities/course.model'; standalone: true, imports: [ArtemisSharedModule, ArtemisSharedComponentModule, ArtemisMarkdownEditorModule, ArtemisCategorySelectorModule], }) -export class FAQUpdateComponent implements OnInit { - faq: FAQ; +export class FaqUpdateComponent implements OnInit { + faq: Faq; isSaving: boolean; isAllowedToSave: boolean; - existingCategories: FAQCategory[]; - faqCategories: FAQCategory[]; + existingCategories: FaqCategory[]; + faqCategories: FaqCategory[]; courseId: number; - courseTitle: string; course: Course; domainActionsDescription = [new FormulaAction()]; @@ -43,7 +42,7 @@ export class FAQUpdateComponent implements OnInit { faBan = faBan; private alertService = inject(AlertService); - private faqService = inject(FAQService); + private faqService = inject(FaqService); private activatedRoute = inject(ActivatedRoute); private navigationUtilService = inject(ArtemisNavigationUtilService); private router = inject(Router); @@ -55,7 +54,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; @@ -63,7 +62,7 @@ export class FAQUpdateComponent implements OnInit { } this.faqCategories = faq?.categories ? faq.categories : []; }); - this.canSave(); + this.validate(); } /** @@ -84,7 +83,7 @@ export class FAQUpdateComponent implements OnInit { if (this.faq.id !== undefined) { this.subscribeToSaveResponse(this.faqService.update(this.courseId, this.faq)); } else { - this.faq.faqState = FAQState.ACCEPTED; + this.faq.faqState = FaqState.ACCEPTED; this.subscribeToSaveResponse(this.faqService.create(this.courseId, this.faq)); } } @@ -92,9 +91,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), }); } @@ -102,10 +101,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(this.courseId, faq.id!).subscribe({ - next: (response: HttpResponse) => { + next: (response: HttpResponse) => { this.isSaving = false; const faqBody = response.body; if (faqBody) { @@ -135,7 +134,7 @@ export class FAQUpdateComponent implements OnInit { } } - updateCategories(categories: FAQCategory[]) { + updateCategories(categories: FaqCategory[]) { this.faq.categories = categories; this.faqCategories = categories; } @@ -146,7 +145,7 @@ export class FAQUpdateComponent implements OnInit { }); } - canSave() { + validate() { if (this.faq.questionTitle && this.faq.questionAnswer) { this.isAllowedToSave = this.faq.questionTitle?.trim().length > 0 && this.faq.questionAnswer?.trim().length > 0; } else { diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index 1dac51a60117..b7be4111cc69 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -72,7 +72,7 @@

    {{ faq.id }} diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index 5d5c576489ce..46c15d9ea049 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -1,14 +1,14 @@ import { Component, OnDestroy, OnInit, inject } 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 { Faq } from 'app/entities/faq.model'; +import { faEdit, faFilter, faPencilAlt, faPlus, 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 { 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'; @@ -23,10 +23,10 @@ import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; standalone: true, imports: [ArtemisSharedModule, CustomExerciseCategoryBadgeComponent, ArtemisSharedComponentModule, ArtemisMarkdownModule], }) -export class FAQComponent implements OnInit, OnDestroy { - faqs: FAQ[]; - filteredFaqs: FAQ[]; - existingCategories: FAQCategory[]; +export class FaqComponent implements OnInit, OnDestroy { + faqs: Faq[]; + filteredFaqs: Faq[]; + existingCategories: FaqCategory[]; courseId: number; hasCategories: boolean = false; @@ -37,21 +37,15 @@ export class FAQComponent implements OnInit, OnDestroy { 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; - protected faqService = inject(FAQService); + private faqService = inject(FaqService); private route = inject(ActivatedRoute); private alertService = inject(AlertService); private sortService = inject(SortService); @@ -71,10 +65,6 @@ export class FAQComponent implements OnInit, OnDestroy { this.dialogErrorSource.complete(); } - trackId(index: number, item: FAQ) { - return item.id; - } - deleteFaq(courseId: number, faqId: number) { this.faqService.delete(courseId, faqId).subscribe({ next: () => this.handleDeleteSuccess(faqId), @@ -86,7 +76,6 @@ export class FAQComponent implements OnInit, OnDestroy { this.faqs = this.faqs.filter((faq) => faq.id !== faqId); this.dialogErrorSource.next(''); this.loadCourseFaqCategories(this.courseId); - this.applyFilters(); } toggleFilters(category: string) { @@ -105,9 +94,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(); }, @@ -119,6 +108,16 @@ export class FAQComponent implements OnInit, OnDestroy { loadCourseFaqCategories(courseId, this.alertService, this.faqService).subscribe((existingCategories) => { this.existingCategories = existingCategories; this.hasCategories = existingCategories.length > 0; + this.checkAppliedFilter(this.activeFilters, this.existingCategories); }); } + + private checkAppliedFilter(activeFilters: Set, existingCategories: FaqCategory[]) { + activeFilters.forEach((activeFilter) => { + if (!existingCategories.some((category) => category.category === activeFilter)) { + activeFilters.delete(activeFilter); + } + }); + this.applyFilters(); + } } diff --git a/src/main/webapp/app/faq/faq.routes.ts b/src/main/webapp/app/faq/faq.routes.ts index 79d961314bea..0b756a8c28ff 100644 --- a/src/main/webapp/app/faq/faq.routes.ts +++ b/src/main/webapp/app/faq/faq.routes.ts @@ -3,22 +3,22 @@ import { HttpResponse } from '@angular/common/http'; import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; import { Observable, of } from 'rxjs'; import { filter, map } from 'rxjs/operators'; -import { FAQService } from 'app/faq/faq.service'; -import { FAQ } from 'app/entities/faq.model'; +import { FaqService } from 'app/faq/faq.service'; +import { Faq } from 'app/entities/faq.model'; @Injectable({ providedIn: 'root' }) -export class FAQResolve implements Resolve { - constructor(private faqService: FAQService) {} +export class FaqResolve implements Resolve { + constructor(private faqService: FaqService) {} - resolve(route: ActivatedRouteSnapshot): Observable { + resolve(route: ActivatedRouteSnapshot): Observable { const faqId = route.params['faqId']; const courseId = route.params['courseId']; if (faqId) { return this.faqService.find(courseId, 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 0beab04e0eaf..d0c80cf72e94 100644 --- a/src/main/webapp/app/faq/faq.service.ts +++ b/src/main/webapp/app/faq/faq.service.ts @@ -2,31 +2,31 @@ 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 { FAQCategory } from 'app/entities/faq-category.model'; +import { Faq, FaqState } from 'app/entities/faq.model'; +import { FaqCategory } from 'app/entities/faq-category.model'; -type EntityResponseType = HttpResponse; -type EntityArrayResponseType = HttpResponse; +type EntityResponseType = HttpResponse; +type EntityArrayResponseType = HttpResponse; @Injectable({ providedIn: 'root' }) -export class FAQService { +export class FaqService { public resourceUrl = 'api/courses'; constructor(protected http: HttpClient) {} - create(courseId: number, faq: FAQ): Observable { - const copy = FAQService.convertFaqFromClient(faq); - copy.faqState = FAQState.ACCEPTED; - return this.http.post(`${this.resourceUrl}/${courseId}/faqs`, copy, { observe: 'response' }).pipe( + create(courseId: number, faq: Faq): Observable { + const copy = FaqService.convertFaqFromClient(faq); + copy.faqState = FaqState.ACCEPTED; + return this.http.post(`${this.resourceUrl}/${courseId}/faqs`, copy, { observe: 'response' }).pipe( map((res: EntityResponseType) => { return res; }), ); } - update(courseId: number, faq: FAQ): Observable { - const copy = FAQService.convertFaqFromClient(faq); - return this.http.put(`${this.resourceUrl}/${courseId}/faqs/${faq.id}`, copy, { observe: 'response' }).pipe( + update(courseId: number, faq: Faq): Observable { + const copy = FaqService.convertFaqFromClient(faq); + return this.http.put(`${this.resourceUrl}/${courseId}/faqs/${faq.id}`, copy, { observe: 'response' }).pipe( map((res: EntityResponseType) => { return res; }), @@ -35,16 +35,16 @@ export class FAQService { find(courseId: number, faqId: number): Observable { return this.http - .get(`${this.resourceUrl}/${courseId}/faqs/${faqId}`, { observe: 'response' }) - .pipe(map((res: EntityResponseType) => FAQService.convertFaqCategoriesFromServer(res))); + .get(`${this.resourceUrl}/${courseId}/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))); + .pipe(map((res: EntityArrayResponseType) => FaqService.convertFaqCategoryArrayFromServer(res))); } delete(courseId: number, faqId: number): Observable> { @@ -62,7 +62,7 @@ export class FAQService { */ static convertFaqCategoriesFromServer(res: ERT): ERT { if (res.body?.categories) { - FAQService.parseFaqCategories(res.body); + FaqService.parseFaqCategories(res.body); } return res; } @@ -71,11 +71,11 @@ 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) { - return faq.categories?.map((category) => JSON.stringify(category) as unknown as FAQCategory); + static stringifyFaqCategories(faq: Faq) { + 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)); } @@ -83,33 +83,33 @@ 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)); + 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) { + 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); }); } } /** * 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 { + static convertFaqFromClient(faq: F): Faq { const copy = Object.assign({}, faq); - copy.categories = FAQService.stringifyFaqCategories(copy); + copy.categories = FaqService.stringifyFaqCategories(copy); return copy; } @@ -122,7 +122,7 @@ export class FAQService { return activeFilters; } - applyFilters(activeFilters: Set, faqs: FAQ[]): FAQ[] { + applyFilters(activeFilters: Set, faqs: Faq[]): Faq[] { if (activeFilters.size === 0) { // If no filters selected, show all faqs return faqs; @@ -131,7 +131,7 @@ export class FAQService { } } - 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/faq/faq.utils.ts b/src/main/webapp/app/faq/faq.utils.ts index 6c083ad5dcde..f96bffdbb575 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-accordion-component.ts b/src/main/webapp/app/overview/course-faq/course-faq-accordion-component.ts index a7948ad4f91c..881a07f2d784 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 74910fccf5ef..51ce0a81b3b2 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,18 +1,18 @@ import { Component, OnDestroy, OnInit, ViewEncapsulation, inject } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { map } from 'rxjs/operators'; import { Subject, Subscription } from 'rxjs'; -import { faFilter, faPlus, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { faFilter } from '@fortawesome/free-solid-svg-icons'; import { ButtonType } from 'app/shared/components/button.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 { 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'; -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'; @@ -30,28 +30,24 @@ export class CourseFaqComponent implements OnInit, OnDestroy { private parentParamSubscription: Subscription; courseId: number; - faqs: FAQ[]; + faqs: Faq[]; - filteredFaqs: FAQ[]; - existingCategories: FAQCategory[]; + filteredFaqs: Faq[]; + existingCategories: FaqCategory[]; activeFilters = new Set(); sidebarData: SidebarData; hasCategories = false; isCollapsed = false; - isProduction = true; - isTestServer = false; readonly ButtonType = ButtonType; // Icons - faPlus = faPlus; - faTimes = faTimes; faFilter = faFilter; private route = inject(ActivatedRoute); - private router = inject(Router); - private faqService = inject(FAQService); + + private faqService = inject(FaqService); private alertService = inject(AlertService); ngOnInit(): void { @@ -72,9 +68,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(); }, 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 983dcc52bd9b..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,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 65640ff2925e..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,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'; diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index 89729023ff10..7b0cffbe63b3 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -88,7 +88,8 @@ "testExam": "Testklausur", "communication": "Kommunikation", "plagiarismCases": "Plagiatsfälle", - "gradingSystem": "Notenschlüssel" + "gradingSystem": "Notenschlüssel", + "faq": "FAQ" }, "exerciseFilter": { "filter": "Filter", diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 7d13035e00d4..bf2bb8fef5b1 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -19,6 +19,7 @@ "notInCourseButCanEnroll": "You are not enrolled in this course, but you can enroll yourself.", "cancel": "Cancel", "coursePrerequisites": "Prerequisites", + "coursePrerequisites": "Prerequisites", "noCourse": "No courses available for enrollment", "courseLoading": "Course is loading...", "coursesLoading": "Courses are loading...", diff --git a/src/test/java/de/tum/cit/aet/artemis/FaqFactory.java b/src/test/java/de/tum/cit/aet/artemis/communication/FaqFactory.java similarity index 94% rename from src/test/java/de/tum/cit/aet/artemis/FaqFactory.java rename to src/test/java/de/tum/cit/aet/artemis/communication/FaqFactory.java index ed782cb289ce..0f8436326876 100644 --- a/src/test/java/de/tum/cit/aet/artemis/FaqFactory.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/FaqFactory.java @@ -1,4 +1,4 @@ -package de.tum.cit.aet.artemis; +package de.tum.cit.aet.artemis.communication; import java.util.HashSet; import java.util.Set; diff --git a/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/FaqIntegrationTest.java similarity index 97% rename from src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java rename to src/test/java/de/tum/cit/aet/artemis/communication/FaqIntegrationTest.java index 2f671040dd29..5f226f52d4dc 100644 --- a/src/test/java/de/tum/cit/aet/artemis/FaqIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/FaqIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.cit.aet.artemis; +package de.tum.cit.aet.artemis.communication; import static org.assertj.core.api.Assertions.assertThat; @@ -18,6 +18,7 @@ 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; +import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; class FaqIntegrationTest extends AbstractSpringIntegrationIndependentTest { @@ -79,7 +80,7 @@ void createFaq_correctRequestBody_shouldCreateFaq() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createFaq_alreadyId_shouldReturnBadRequest() throws Exception { Faq newFaq = FaqFactory.generateFaq(course1, FaqState.ACCEPTED, "title", "answer"); - faq.setId(this.faq.getId()); + newFaq.setId(this.faq.getId()); request.postWithResponseBody("/api/courses/" + course1.getId() + "/faqs", newFaq, Faq.class, HttpStatus.BAD_REQUEST); } 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 98ec83cb8340..200db35cb744 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 @@ -9,38 +9,38 @@ import { MockRouterLinkDirective } from '../../helpers/mocks/directive/mock-rout 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, FAQState } from 'app/entities/faq.model'; +import { FaqUpdateComponent } from 'app/faq/faq-update.component'; +import { FaqService } from 'app/faq/faq.service'; +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'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AlertService } from 'app/core/util/alert.service'; -import { FAQCategory } from 'app/entities/faq-category.model'; +import { FaqCategory } from 'app/entities/faq-category.model'; describe('FaqUpdateComponent', () => { - let faqUpdateComponentFixture: ComponentFixture; - let faqUpdateComponent: FAQUpdateComponent; - let faqService: FAQService; + let faqUpdateComponentFixture: ComponentFixture; + let faqUpdateComponent: FaqUpdateComponent; + let faqService: FaqService; let activatedRoute: ActivatedRoute; let router: Router; - let faq1: FAQ; + let faq1: Faq; let courseId: number; let alertServiceStub: jest.SpyInstance; let alertService: AlertService; beforeEach(() => { - faq1 = new FAQ(); + faq1 = new Faq(); faq1.id = 1; faq1.questionTitle = 'questionTitle'; faq1.questionAnswer = 'questionAnswer'; - faq1.categories = [new FAQCategory('category1', '#94a11c')]; + faq1.categories = [new FaqCategory('category1', '#94a11c')]; courseId = 1; TestBed.configureTestingModule({ imports: [ArtemisTestModule, MonacoEditorModule, MockModule(BrowserAnimationsModule)], - declarations: [FAQUpdateComponent, MockComponent(MonacoEditorComponent), MockPipe(HtmlForMarkdownPipe), MockRouterLinkDirective], + declarations: [FaqUpdateComponent, MockComponent(MonacoEditorComponent), MockPipe(HtmlForMarkdownPipe), MockRouterLinkDirective], providers: [ { provide: TranslateService, useClass: MockTranslateService }, { provide: Router, useClass: MockRouter }, @@ -57,7 +57,7 @@ describe('FaqUpdateComponent', () => { }, }, }, - MockProvider(FAQService, { + MockProvider(FaqService, { find: () => { return of( new HttpResponse({ @@ -81,10 +81,10 @@ describe('FaqUpdateComponent', () => { global.ResizeObserver = jest.fn().mockImplementation((callback: ResizeObserverCallback) => { return new MockResizeObserver(callback); }); - faqUpdateComponentFixture = TestBed.createComponent(FAQUpdateComponent); + faqUpdateComponentFixture = TestBed.createComponent(FaqUpdateComponent); faqUpdateComponent = faqUpdateComponentFixture.componentInstance; - faqService = TestBed.inject(FAQService); + faqService = TestBed.inject(FaqService); alertService = TestBed.inject(AlertService); router = TestBed.inject(Router); @@ -97,8 +97,7 @@ describe('FaqUpdateComponent', () => { }); it('should create faq', fakeAsync(() => { - faqUpdateComponent.faq = { questionTitle: 'test1' } as FAQ; - faq1.categories = undefined; + faqUpdateComponent.faq = { questionTitle: 'test1' } as Faq; const createSpy = jest.spyOn(faqService, 'create').mockReturnValue( of( new HttpResponse({ @@ -108,7 +107,7 @@ describe('FaqUpdateComponent', () => { course: { id: 1, }, - } as FAQ, + } as Faq, }), ), ); @@ -117,7 +116,7 @@ describe('FaqUpdateComponent', () => { faqUpdateComponent.save(); tick(); - expect(createSpy).toHaveBeenCalledExactlyOnceWith(courseId, { faqState: FAQState.ACCEPTED, questionTitle: 'test1' }); + expect(createSpy).toHaveBeenCalledExactlyOnceWith(courseId, { faqState: FaqState.ACCEPTED, questionTitle: 'test1' }); expect(faqUpdateComponent.isSaving).toBeFalse(); })); @@ -125,10 +124,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, @@ -137,7 +136,7 @@ describe('FaqUpdateComponent', () => { course: { id: 1, }, - } as FAQ, + } as Faq, }), ), ); @@ -146,8 +145,7 @@ describe('FaqUpdateComponent', () => { tick(); faqUpdateComponentFixture.detectChanges(); - expect(updateSpy).toHaveBeenCalledOnce(); - expect(updateSpy).toHaveBeenCalledWith(courseId, { id: 6, questionTitle: 'test1Updated' }); + expect(updateSpy).toHaveBeenCalledExactlyOnceWith(courseId, { id: 6, questionTitle: 'test1Updated' }); })); it('should navigate to previous state', fakeAsync(() => { @@ -168,7 +166,7 @@ describe('FaqUpdateComponent', () => { })); it('should update categories', fakeAsync(() => { - const categories = [new FAQCategory('category1', 'red'), new FAQCategory('category2', 'blue')]; + const categories = [new FaqCategory('category1', 'red'), new FaqCategory('category2', 'blue')]; faqUpdateComponentFixture.detectChanges(); faqUpdateComponent.updateCategories(categories); expect(faqUpdateComponent.faqCategories).toEqual(categories); @@ -177,14 +175,14 @@ describe('FaqUpdateComponent', () => { it('should not be able to save unless title and question are filled', fakeAsync(() => { faqUpdateComponentFixture.detectChanges(); - faqUpdateComponent.faq = { questionTitle: 'test1' } as FAQ; - faqUpdateComponent.canSave(); + faqUpdateComponent.faq = { questionTitle: 'test1' } as Faq; + faqUpdateComponent.validate(); expect(faqUpdateComponent.isAllowedToSave).toBeFalse(); - faqUpdateComponent.faq = { questionAnswer: 'test1' } as FAQ; - faqUpdateComponent.canSave(); + faqUpdateComponent.faq = { questionAnswer: 'test1' } as Faq; + faqUpdateComponent.validate(); expect(faqUpdateComponent.isAllowedToSave).toBeFalse(); - faqUpdateComponent.faq = { questionTitle: 'test', questionAnswer: 'test1' } as FAQ; - faqUpdateComponent.canSave(); + faqUpdateComponent.faq = { questionTitle: 'test', questionAnswer: 'test1' } as Faq; + faqUpdateComponent.validate(); expect(faqUpdateComponent.isAllowedToSave).toBeTrue(); })); 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 613cbcf8f58f..d31f60b8e2f2 100644 --- a/src/test/javascript/spec/component/faq/faq.component.spec.ts +++ b/src/test/javascript/spec/component/faq/faq.component.spec.ts @@ -8,39 +8,38 @@ import { MockRouterLinkDirective } from '../../helpers/mocks/directive/mock-rout 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 { 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 { 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'; import { AlertService } from 'app/core/util/alert.service'; import { SortService } from 'app/shared/service/sort.service'; -function createFaq(id: number, category: string, color: string): FAQ { - const faq = new FAQ(); +function createFaq(id: number, category: string, color: string): Faq { + const faq = new Faq(); faq.id = id; faq.questionTitle = 'questionTitle'; faq.questionAnswer = 'questionAnswer'; - faq.categories = [new FAQCategory(category, color)]; + faq.categories = [new FaqCategory(category, color)]; return faq; } describe('FaqComponent', () => { - let faqComponentFixture: ComponentFixture; - let faqComponent: FAQComponent; + let faqComponentFixture: ComponentFixture; + let faqComponent: FaqComponent; - let faqService: FAQService; + let faqService: FaqService; let alertServiceStub: jest.SpyInstance; let alertService: AlertService; let sortService: SortService; - let faq1: FAQ; - let faq2: FAQ; - let faq3: FAQ; + let faq1: Faq; + let faq2: Faq; + let faq3: Faq; let courseId: number; @@ -53,8 +52,8 @@ describe('FaqComponent', () => { courseId = 1; TestBed.configureTestingModule({ - imports: [ArtemisTestModule, ArtemisMarkdownEditorModule, MockModule(BrowserAnimationsModule)], - declarations: [FAQComponent, MockRouterLinkDirective, MockComponent(CustomExerciseCategoryBadgeComponent)], + imports: [ArtemisTestModule, MockModule(ArtemisMarkdownEditorModule), MockModule(BrowserAnimationsModule)], + declarations: [FaqComponent, MockRouterLinkDirective, MockComponent(CustomExerciseCategoryBadgeComponent)], providers: [ { provide: TranslateService, useClass: MockTranslateService }, { provide: Router, useClass: MockRouter }, @@ -71,7 +70,7 @@ describe('FaqComponent', () => { }, }, }, - MockProvider(FAQService, { + MockProvider(FaqService, { findAllByCourseId: () => { return of( new HttpResponse({ @@ -99,13 +98,10 @@ describe('FaqComponent', () => { }) .compileComponents() .then(() => { - global.ResizeObserver = jest.fn().mockImplementation((callback: ResizeObserverCallback) => { - return new MockResizeObserver(callback); - }); - faqComponentFixture = TestBed.createComponent(FAQComponent); + faqComponentFixture = TestBed.createComponent(FaqComponent); faqComponent = faqComponentFixture.componentInstance; - faqService = TestBed.inject(FAQService); + faqService = TestBed.inject(FaqService); alertService = TestBed.inject(AlertService); sortService = TestBed.inject(SortService); }); @@ -136,8 +132,7 @@ describe('FaqComponent', () => { const deleteSpy = jest.spyOn(faqService, 'delete'); faqComponentFixture.detectChanges(); faqComponent.deleteFaq(courseId, faq1.id!); - expect(deleteSpy).toHaveBeenCalledOnce(); - expect(deleteSpy).toHaveBeenCalledWith(courseId, faq1.id!); + expect(deleteSpy).toHaveBeenCalledExactlyOnceWith(courseId, faq1.id!); expect(faqComponent.faqs).toHaveLength(2); expect(faqComponent.faqs).not.toContain(faq1); expect(faqComponent.faqs).toEqual(faqComponent.filteredFaqs); @@ -148,8 +143,7 @@ describe('FaqComponent', () => { const deleteSpy = jest.spyOn(faqService, 'delete').mockReturnValue(throwError(() => new HttpErrorResponse(error))); faqComponentFixture.detectChanges(); faqComponent.deleteFaq(courseId, faq1.id!); - expect(deleteSpy).toHaveBeenCalledOnce(); - expect(deleteSpy).toHaveBeenCalledWith(courseId, faq1.id!); + expect(deleteSpy).toHaveBeenCalledExactlyOnceWith(courseId, faq1.id!); expect(faqComponent.faqs).toHaveLength(3); expect(faqComponent.faqs).toContain(faq1); }); diff --git a/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts index 8873f453dce9..6c5a21b56ddb 100644 --- a/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts @@ -10,21 +10,21 @@ import { TranslateDirective } from 'app/shared/language/translate.directive'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { CourseFaqComponent } from 'app/overview/course-faq/course-faq.component'; import { AlertService } from 'app/core/util/alert.service'; -import { FAQService } from 'app/faq/faq.service'; +import { FaqService } from 'app/faq/faq.service'; import { MockRouter } from '../../../helpers/mocks/mock-router'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; import { CourseFaqAccordionComponent } from 'app/overview/course-faq/course-faq-accordion-component'; -import { FAQ } from 'app/entities/faq.model'; -import { FAQCategory } from 'app/entities/faq-category.model'; +import { Faq } from 'app/entities/faq.model'; +import { FaqCategory } from 'app/entities/faq-category.model'; -function createFaq(id: number, category: string, color: string): FAQ { - const faq = new FAQ(); +function createFaq(id: number, category: string, color: string): Faq { + const faq = new Faq(); faq.id = id; faq.questionTitle = 'questionTitle'; faq.questionAnswer = 'questionAnswer'; - faq.categories = [new FAQCategory(category, color)]; + faq.categories = [new FaqCategory(category, color)]; return faq; } @@ -32,13 +32,13 @@ describe('CourseFaqs', () => { let courseFaqComponentFixture: ComponentFixture; let courseFaqComponent: CourseFaqComponent; - let faqService: FAQService; + let faqService: FaqService; let alertServiceStub: jest.SpyInstance; let alertService: AlertService; - let faq1: FAQ; - let faq2: FAQ; - let faq3: FAQ; + let faq1: Faq; + let faq2: Faq; + let faq3: Faq; beforeEach(() => { // In beforeEach: @@ -50,7 +50,7 @@ describe('CourseFaqs', () => { imports: [ArtemisSharedComponentModule, ArtemisSharedModule, MockComponent(CustomExerciseCategoryBadgeComponent), MockComponent(CourseFaqAccordionComponent)], declarations: [CourseFaqComponent, MockPipe(ArtemisTranslatePipe), MockComponent(FaIconComponent), MockDirective(TranslateDirective)], providers: [ - MockProvider(FAQService), + MockProvider(FaqService), { provide: Router, useClass: MockRouter }, { provide: TranslateService, useClass: MockTranslateService }, { @@ -61,7 +61,7 @@ describe('CourseFaqs', () => { }, }, }, - MockProvider(FAQService, { + MockProvider(FaqService, { findAllByCourseId: () => { return of( new HttpResponse({ @@ -92,7 +92,7 @@ describe('CourseFaqs', () => { courseFaqComponentFixture = TestBed.createComponent(CourseFaqComponent); courseFaqComponent = courseFaqComponentFixture.componentInstance; - faqService = TestBed.inject(FAQService); + faqService = TestBed.inject(FaqService); alertService = TestBed.inject(AlertService); }); }); diff --git a/src/test/javascript/spec/service/faq.service.spec.ts b/src/test/javascript/spec/service/faq.service.spec.ts index da324b38c1bf..b5612f991ebd 100644 --- a/src/test/javascript/spec/service/faq.service.spec.ts +++ b/src/test/javascript/spec/service/faq.service.spec.ts @@ -8,15 +8,15 @@ 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 { FAQCategory } from 'app/entities/faq-category.model'; -import { FAQService } from 'app/faq/faq.service'; +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; + let service: FaqService; let expectedResult: any; - let elemDefault: FAQ; + let elemDefault: Faq; let courseId: number; beforeEach(() => { @@ -28,16 +28,16 @@ describe('Faq Service', () => { { provide: TranslateService, useClass: MockTranslateService }, ], }); - service = TestBed.inject(FAQService); + 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'; elemDefault.id = 1; - elemDefault.faqState = FAQState.ACCEPTED; + elemDefault.faqState = FaqState.ACCEPTED; courseId = 1; }); @@ -96,9 +96,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(courseId, faqId) @@ -116,9 +116,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) @@ -136,7 +136,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; @@ -172,14 +172,14 @@ describe('Faq Service', () => { it('should apply faqFilter correctly', () => { const activeFilters = new Set(); - const faq1 = new FAQ(); - faq1.categories = [new FAQCategory('test', 'red'), new FAQCategory('test2', 'blue')]; + 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 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')]; + const faq2 = new Faq(); + faq2.categories = [new FaqCategory('testing', 'red'), new FaqCategory('test2', 'blue')]; let filteredFaq = [faq1, faq11, faq2]; @@ -200,9 +200,9 @@ describe('Faq Service', () => { }); it('should convert FAQ categories into strings', () => { - const faq2 = new FAQ(); - faq2.categories = [new FAQCategory('testing', 'red')]; - const convertedCategory = FAQService.stringifyFaqCategories(faq2); + const faq2 = new Faq(); + faq2.categories = [new FaqCategory('testing', 'red')]; + const convertedCategory = FaqService.stringifyFaqCategories(faq2); expect(convertedCategory).toEqual(['{"color":"red","category":"testing"}']); }); }); From 2d25d30004898d1bcef31156f7d5dca30f1d8a46 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Sun, 29 Sep 2024 10:43:18 +0200 Subject: [PATCH 095/107] remove duplicate --- src/main/webapp/i18n/en/student-dashboard.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index bf2bb8fef5b1..7d13035e00d4 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -19,7 +19,6 @@ "notInCourseButCanEnroll": "You are not enrolled in this course, but you can enroll yourself.", "cancel": "Cancel", "coursePrerequisites": "Prerequisites", - "coursePrerequisites": "Prerequisites", "noCourse": "No courses available for enrollment", "courseLoading": "Course is loading...", "coursesLoading": "Courses are loading...", From 8ae824c035bb10f70f075ea4bf1d88ff4a2fc857 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Sun, 29 Sep 2024 11:19:18 +0200 Subject: [PATCH 096/107] remove duplicate --- .../de/tum/cit/aet/artemis/communication/web/FaqResource.java | 3 ++- 1 file changed, 2 insertions(+), 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 7cd7acc93551..ad163b2f451f 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 @@ -65,7 +65,8 @@ public FaqResource(CourseRepository courseRepository, AuthorizationCheckService /** * POST /courses/:courseId/faqs : Create a new faq. * - * @param faq the faq to create + * @param faq the faq to create * + * @param courseId the id of the course the faq belongs to * @return the ResponseEntity with status 201 (Created) and with body the new faq, or with status 400 (Bad Request) if the faq has already an ID * or with status 500 if the faq course id does not match with the path variable * @throws URISyntaxException if the Location URI syntax is incorrect From 2ce0631c83d1470bb09ab5b329adcac03e805800 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Sun, 29 Sep 2024 12:01:32 +0200 Subject: [PATCH 097/107] fixed coderabit issues --- .../cit/aet/artemis/communication/web/FaqResource.java | 8 ++++---- src/main/webapp/app/faq/faq-update.component.html | 4 ++-- src/main/webapp/app/faq/faq-update.component.ts | 3 --- 3 files changed, 6 insertions(+), 9 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 ad163b2f451f..b76578ba2c0f 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 @@ -67,8 +67,8 @@ public FaqResource(CourseRepository courseRepository, AuthorizationCheckService * * @param faq the faq to create * * @param courseId the id of the course the faq belongs to - * @return the ResponseEntity with status 201 (Created) and with body the new faq, or with status 400 (Bad Request) if the faq has already an ID - * or with status 500 if the faq course id does not match with the path variable + * @return the ResponseEntity with status 201 (Created) and with body the new faq, or with status 400 (Bad Request) + * if the faq has already an ID or if the faq course id does not match with the path variable * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("courses/{courseId}/faqs") @@ -95,8 +95,8 @@ public ResponseEntity createFaq(@RequestBody Faq faq, @PathVariable Long * @param faq the faq to update * @param faqId id of the faq to be updated * * @param courseId the id of the course the faq belongs to - * @return the ResponseEntity with status 200 (OK) and with body the updated faq, or with status 400 (Bad Request) if the faq is not valid, or with status 500 (Internal - * Server Error) if the faq couldn't 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 if the faq course id does not match with the path variable */ @PutMapping("courses/{courseId}/faqs/{faqId}") @EnforceAtLeastInstructor diff --git a/src/main/webapp/app/faq/faq-update.component.html b/src/main/webapp/app/faq/faq-update.component.html index c812877b6637..ab4e33050798 100644 --- a/src/main/webapp/app/faq/faq-update.component.html +++ b/src/main/webapp/app/faq/faq-update.component.html @@ -10,9 +10,9 @@

    - +
    - +
    diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index 5e5c4aae5103..bc4ae290f97c 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -16,7 +16,6 @@ import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown 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'; -import { Course } from 'app/entities/course.model'; @Component({ selector: 'jhi-faq-update', @@ -32,8 +31,6 @@ export class FaqUpdateComponent implements OnInit { existingCategories: FaqCategory[]; faqCategories: FaqCategory[]; courseId: number; - course: Course; - domainActionsDescription = [new FormulaAction()]; // Icons From 76a1e531f6201856596296493ed9357747807e4e Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Sun, 29 Sep 2024 12:23:10 +0200 Subject: [PATCH 098/107] fixed coderabit issues --- src/main/webapp/app/faq/faq.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/faq/faq.component.html b/src/main/webapp/app/faq/faq.component.html index b7be4111cc69..94bdab39fb48 100644 --- a/src/main/webapp/app/faq/faq.component.html +++ b/src/main/webapp/app/faq/faq.component.html @@ -106,7 +106,7 @@

    [entityTitle]="faq.questionTitle || ''" deleteQuestion="artemisApp.faq.delete.question" deleteConfirmationText="artemisApp.faq.delete.typeNameToConfirm" - (delete)="deleteFaq(this.courseId, faq.id!)" + (delete)="deleteFaq(courseId, faq.id!)" [dialogError]="dialogError$" > From 8bf74af9468087c2c86befb76aa338f8354cde87 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Mon, 30 Sep 2024 06:51:29 +0200 Subject: [PATCH 099/107] Removed Resize Observer --- .../spec/component/faq/faq-update.component.spec.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) 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 200db35cb744..04c04b3d12d3 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 @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed, fakeAsync, flush, tick } from '@angular/core 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 { MockModule, MockPipe, MockProvider } from 'ng-mocks'; import { of, throwError } from 'rxjs'; import { MockRouterLinkDirective } from '../../helpers/mocks/directive/mock-router-link.directive'; import { MockRouter } from '../../helpers/mocks/mock-router'; @@ -12,12 +12,10 @@ import { ArtemisTestModule } from '../../test.module'; import { FaqUpdateComponent } from 'app/faq/faq-update.component'; import { FaqService } from 'app/faq/faq.service'; 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'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AlertService } from 'app/core/util/alert.service'; import { FaqCategory } from 'app/entities/faq-category.model'; +import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; describe('FaqUpdateComponent', () => { let faqUpdateComponentFixture: ComponentFixture; @@ -39,8 +37,8 @@ describe('FaqUpdateComponent', () => { faq1.categories = [new FaqCategory('category1', '#94a11c')]; courseId = 1; TestBed.configureTestingModule({ - imports: [ArtemisTestModule, MonacoEditorModule, MockModule(BrowserAnimationsModule)], - declarations: [FaqUpdateComponent, MockComponent(MonacoEditorComponent), MockPipe(HtmlForMarkdownPipe), MockRouterLinkDirective], + imports: [ArtemisTestModule, MockModule(ArtemisMarkdownEditorModule), MockModule(BrowserAnimationsModule)], + declarations: [FaqUpdateComponent, MockPipe(HtmlForMarkdownPipe), MockRouterLinkDirective], providers: [ { provide: TranslateService, useClass: MockTranslateService }, { provide: Router, useClass: MockRouter }, @@ -78,9 +76,6 @@ describe('FaqUpdateComponent', () => { ], }).compileComponents(); - global.ResizeObserver = jest.fn().mockImplementation((callback: ResizeObserverCallback) => { - return new MockResizeObserver(callback); - }); faqUpdateComponentFixture = TestBed.createComponent(FaqUpdateComponent); faqUpdateComponent = faqUpdateComponentFixture.componentInstance; From 48cb712158c296d8eaddb3d2dcffce8eb93d4a0b Mon Sep 17 00:00:00 2001 From: TIm Cremer Date: Tue, 1 Oct 2024 15:32:30 +0200 Subject: [PATCH 100/107] Added lines for the overview, sorted per default --- src/main/webapp/app/faq/faq.component.ts | 1 + .../course-faq/course-faq-accordion.component.html | 3 ++- .../course-faq/course-faq-accordion.component.scss | 4 ---- .../app/overview/course-faq/course-faq.component.html | 7 +++---- .../app/overview/course-faq/course-faq.component.scss | 6 ------ src/main/webapp/app/overview/courses-routing.module.ts | 2 +- 6 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index 46c15d9ea049..3790932a47a8 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -99,6 +99,7 @@ export class FaqComponent implements OnInit, OnDestroy { next: (res: Faq[]) => { this.faqs = res; this.applyFilters(); + this.sortRows(); }, error: (res: HttpErrorResponse) => onError(this.alertService, 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 b3f278459857..ad69bb5c454f 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 @@ -8,7 +8,8 @@

    {{ faq().questionTitle }}

    }

    -
    +

    +
    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 0db97794d8c8..9b56cb9aac9f 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 @@ -6,10 +6,6 @@ box-sizing: border-box; } -.faq-container h2 { - margin: 0; -} - .badge-container { display: flex; margin-left: auto; 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 ff249d442a9f..c94960161ee4 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,11 +25,10 @@ } -
    +
    +
    @for (faq of this.filteredFaqs; 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 9a444afe91a8..9e1c700ded25 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 @@ -1,9 +1,3 @@ -.scroll-container { - max-height: 83vh; - overflow-y: auto; - overflow-x: hidden; -} - .second-layer-modal-bg { background-color: var(--secondary); } diff --git a/src/main/webapp/app/overview/courses-routing.module.ts b/src/main/webapp/app/overview/courses-routing.module.ts index c962535ad191..4cb31090febf 100644 --- a/src/main/webapp/app/overview/courses-routing.module.ts +++ b/src/main/webapp/app/overview/courses-routing.module.ts @@ -261,7 +261,7 @@ const routes: Routes = [ data: { authorities: [Authority.USER], pageTitle: 'overview.faq', - hasSidebar: true, + hasSidebar: false, showRefreshButton: true, }, }, From dfe9e434b8a009fd60beedb105675bd6f2cb4b82 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 1 Oct 2024 18:47:00 +0200 Subject: [PATCH 101/107] Refactor database column names to contain singular --- .../de/tum/cit/aet/artemis/communication/domain/Faq.java | 4 ++-- .../artemis/communication/repository/FaqRepository.java | 5 ----- .../liquibase/changelog/20240902175045_changelog.xml | 8 ++++---- .../webapp/app/course/manage/course-update.component.ts | 1 - src/main/webapp/i18n/de/course.json | 2 +- src/main/webapp/i18n/de/faq.json | 4 ++-- src/main/webapp/i18n/en/course.json | 2 +- 7 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/Faq.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/Faq.java index 061fb0c2d817..fd7ce4fca468 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/Faq.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/Faq.java @@ -39,8 +39,8 @@ public class Faq extends AbstractAuditingEntity { private String questionAnswer; @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "faq_categories", joinColumns = @JoinColumn(name = "faq_id")) - @Column(name = "categories") + @CollectionTable(name = "faq_category", joinColumns = @JoinColumn(name = "faq_id")) + @Column(name = "category") private Set categories = new HashSet<>(); @Enumerated(EnumType.STRING) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java index d1584d66fcd3..47f8798197e7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java @@ -21,11 +21,6 @@ @Repository public interface FaqRepository extends ArtemisJpaRepository { - @Query(""" - SELECT faq - FROM Faq faq - WHERE faq.course.id = :courseId - """) Set findAllByCourseId(@Param("courseId") Long courseId); @Query(""" diff --git a/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml b/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml index 3f2400598da3..75859ab0c57f 100644 --- a/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml @@ -26,11 +26,11 @@ - + - + @@ -47,8 +47,8 @@ referencedTableName="course" validate="true"/> 0 || this.course.maxTeamComplaints! > 0) && this.course.maxComplaintTimeDays! > 0 && - this.course.maxComplaintTimeDays! > 0 && this.course.maxComplaintTextLimit! > 0 && this.course.maxComplaintResponseTextLimit! > 0; this.requestMoreFeedbackEnabled = this.course.maxRequestMoreFeedbackTimeDays! > 0; diff --git a/src/main/webapp/i18n/de/course.json b/src/main/webapp/i18n/de/course.json index c952f7226a3b..a69b777b2113 100644 --- a/src/main/webapp/i18n/de/course.json +++ b/src/main/webapp/i18n/de/course.json @@ -108,7 +108,7 @@ "codeOfConduct": "Nachrichten: Code of Conduct" }, "faqEnabled": { - "label": "FAQ aktivieren", + "label": "FAQs aktivieren", "tooltip": "Ermöglicht das Anlegen von FAQ-Einträgen, in denen Lehrende häufig gestellte Fragen übersichtlich sammeln. Studierende können auf diese Wissensbasis zugreifen, um selbstständig Themen nachzuarbeiten und offene Fragen eigenständig zu klären." } }, diff --git a/src/main/webapp/i18n/de/faq.json b/src/main/webapp/i18n/de/faq.json index 7b8b2aa90c97..51253eb7bd8c 100644 --- a/src/main/webapp/i18n/de/faq.json +++ b/src/main/webapp/i18n/de/faq.json @@ -11,8 +11,8 @@ "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." + "question": "Soll die FAQ {{ title }} wirklich dauerhaft gelöscht werden? Diese Aktion kann NICHT rückgängig gemacht werden!", + "typeNameToConfirm": "Bitte gib der Namen des FAQ zur Bestätigung ein." }, "table": { diff --git a/src/main/webapp/i18n/en/course.json b/src/main/webapp/i18n/en/course.json index 6a6a2f819343..431581743ad1 100644 --- a/src/main/webapp/i18n/en/course.json +++ b/src/main/webapp/i18n/en/course.json @@ -108,7 +108,7 @@ "codeOfConduct": "Messaging Code of Conduct" }, "faqEnabled": { - "label": "FAQ Enabled", + "label": "FAQ enabled", "tooltip": "Enables the creation of FAQ entries where instructors can compile frequently asked questions in an organized manner. Students can access this knowledge base to independently review topics and resolve their questions on their own." } }, From cbdaffaf05b08d0b6b5d4b3a9b4875cf25a0b388 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 1 Oct 2024 19:38:59 +0200 Subject: [PATCH 102/107] Fixed translation --- .../aet/artemis/communication/repository/FaqRepository.java | 5 ----- src/main/webapp/i18n/de/faq.json | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java index 47f8798197e7..d6f8bc8204c1 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java @@ -32,11 +32,6 @@ public interface FaqRepository extends ArtemisJpaRepository { @Transactional @Modifying - @Query(""" - DELETE - FROM Faq faq - WHERE faq.course.id = :courseId - """) void deleteAllByCourseId(@Param("courseId") Long courseId); } diff --git a/src/main/webapp/i18n/de/faq.json b/src/main/webapp/i18n/de/faq.json index 51253eb7bd8c..0cb07d298310 100644 --- a/src/main/webapp/i18n/de/faq.json +++ b/src/main/webapp/i18n/de/faq.json @@ -12,7 +12,7 @@ "deleted": "Das FAQ wurde erfolgreich gelöscht", "delete": { "question": "Soll die FAQ {{ title }} wirklich dauerhaft gelöscht werden? Diese Aktion kann NICHT rückgängig gemacht werden!", - "typeNameToConfirm": "Bitte gib der Namen des FAQ zur Bestätigung ein." + "typeNameToConfirm": "Bitte gib den Namen des FAQ zur Bestätigung ein." }, "table": { From 9c1b5a625faf3b26e9bc16050acfd0c0ae5ba1f2 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 1 Oct 2024 20:21:36 +0200 Subject: [PATCH 103/107] css fix --- .../app/overview/course-faq/course-faq-accordion.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ad69bb5c454f..61352fbc2235 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 @@ -4,7 +4,7 @@

    {{ faq().questionTitle }}

    @for (category of faq().categories; track category) { - + }
    From b42ec4f3af2499c4335631fb7119eab3d6aa5c73 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 1 Oct 2024 20:33:14 +0200 Subject: [PATCH 104/107] removed validation of markdown editor --- src/main/webapp/app/faq/faq-update.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index bc4ae290f97c..cb0af3454515 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -143,8 +143,8 @@ export class FaqUpdateComponent implements OnInit { } validate() { - if (this.faq.questionTitle && this.faq.questionAnswer) { - this.isAllowedToSave = this.faq.questionTitle?.trim().length > 0 && this.faq.questionAnswer?.trim().length > 0; + if (this.faq.questionTitle) { + this.isAllowedToSave = this.faq.questionTitle?.trim().length > 0; } else { this.isAllowedToSave = false; } From 5122a10e427f875da74a98e65e6cfbf67dc02f18 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 2 Oct 2024 08:17:54 +0200 Subject: [PATCH 105/107] Solve markdown issues --- .../artemis/communication/repository/FaqRepository.java | 2 ++ .../cit/aet/artemis/communication/web/FaqResource.java | 2 +- src/main/webapp/app/faq/faq-update.component.html | 4 ++-- src/main/webapp/app/faq/faq-update.component.ts | 9 +++++++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java index d6f8bc8204c1..e223ef0ae669 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java @@ -21,6 +21,7 @@ @Repository public interface FaqRepository extends ArtemisJpaRepository { + @Query Set findAllByCourseId(@Param("courseId") Long courseId); @Query(""" @@ -32,6 +33,7 @@ public interface FaqRepository extends ArtemisJpaRepository { @Transactional @Modifying + @Query void deleteAllByCourseId(@Param("courseId") Long 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 b76578ba2c0f..91a542aaa220 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 @@ -90,7 +90,7 @@ public ResponseEntity createFaq(@RequestBody Faq faq, @PathVariable Long } /** - * PUT /courses/:courseId/faqs/{faqId} : Updates an existing faq. + * PUT /courses/:courseId/faqs/:faqId : Updates an existing faq. * * @param faq the faq to update * @param faqId id of the faq to be updated * diff --git a/src/main/webapp/app/faq/faq-update.component.html b/src/main/webapp/app/faq/faq-update.component.html index ab4e33050798..51211a6bb77e 100644 --- a/src/main/webapp/app/faq/faq-update.component.html +++ b/src/main/webapp/app/faq/faq-update.component.html @@ -21,8 +21,8 @@

    diff --git a/src/main/webapp/app/faq/faq-update.component.ts b/src/main/webapp/app/faq/faq-update.component.ts index cb0af3454515..17e3bd2d16d9 100644 --- a/src/main/webapp/app/faq/faq-update.component.ts +++ b/src/main/webapp/app/faq/faq-update.component.ts @@ -143,10 +143,15 @@ export class FaqUpdateComponent implements OnInit { } validate() { - if (this.faq.questionTitle) { - this.isAllowedToSave = this.faq.questionTitle?.trim().length > 0; + if (this.faq.questionTitle && this.faq.questionAnswer) { + this.isAllowedToSave = this.faq.questionTitle?.trim().length > 0 && this.faq.questionAnswer?.trim().length > 0; } else { this.isAllowedToSave = false; } } + + handleMarkdownChange(markdown: string): void { + this.faq.questionAnswer = markdown; + this.validate(); + } } From 7ad62dc5f5bd67bd6c5deba683178eac7cfb888c Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 2 Oct 2024 08:58:22 +0200 Subject: [PATCH 106/107] Fixed server errors --- .../aet/artemis/communication/repository/FaqRepository.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java index e223ef0ae669..bd8bb8989995 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java @@ -21,8 +21,7 @@ @Repository public interface FaqRepository extends ArtemisJpaRepository { - @Query - Set findAllByCourseId(@Param("courseId") Long courseId); + Set findAllByCourseId(Long courseId); @Query(""" SELECT DISTINCT faq.categories @@ -33,7 +32,6 @@ public interface FaqRepository extends ArtemisJpaRepository { @Transactional @Modifying - @Query - void deleteAllByCourseId(@Param("courseId") Long courseId); + void deleteAllByCourseId(Long courseId); } From 80fd09056f79860f38e2a998dbfe475fc1404a61 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 2 Oct 2024 13:52:56 +0200 Subject: [PATCH 107/107] Matched FAQ categories size to Exercise Categories --- .../config/liquibase/changelog/20240902175045_changelog.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml b/src/main/resources/config/liquibase/changelog/20240902175045_changelog.xml index 75859ab0c57f..6344b448df92 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 @@ - +