Skip to content

Commit

Permalink
First draft implementation: Propose FAQ as editor
Browse files Browse the repository at this point in the history
  • Loading branch information
cremertim committed Oct 9, 2024
1 parent 2e5ebe5 commit 0544837
Show file tree
Hide file tree
Showing 12 changed files with 166 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.springframework.transaction.annotation.Transactional;

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.repository.base.ArtemisJpaRepository;

/**
Expand All @@ -30,6 +31,8 @@ public interface FaqRepository extends ArtemisJpaRepository<Faq, Long> {
""")
Set<String> findAllCategoriesByCourseId(@Param("courseId") Long courseId);

Set<Faq> findAllByCourseIdAndFaqState(Long courseId, FaqState faqState);

@Transactional
@Modifying
void deleteAllByCourseId(Long courseId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@
import org.springframework.web.bind.annotation.RestController;

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.dto.FaqDTO;
import de.tum.cit.aet.artemis.communication.repository.FaqRepository;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.repository.CourseRepository;
import de.tum.cit.aet.artemis.core.security.Role;
import de.tum.cit.aet.artemis.core.security.annotations.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;
Expand Down Expand Up @@ -72,7 +74,7 @@ public FaqResource(CourseRepository courseRepository, AuthorizationCheckService
* @throws URISyntaxException if the Location URI syntax is incorrect
*/
@PostMapping("courses/{courseId}/faqs")
@EnforceAtLeastInstructor
@EnforceAtLeastEditor
public ResponseEntity<FaqDTO> createFaq(@RequestBody Faq faq, @PathVariable Long courseId) throws URISyntaxException {
log.debug("REST request to save Faq : {}", faq);
if (faq.getId() != null) {
Expand All @@ -82,7 +84,7 @@ public ResponseEntity<FaqDTO> createFaq(@RequestBody Faq faq, @PathVariable Long
if (faq.getCourse() == null || !faq.getCourse().getId().equals(courseId)) {
throw new BadRequestAlertException("Course ID in path and FAQ do not match", ENTITY_NAME, "courseIdMismatch");
}
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null);
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, faq.getCourse(), null);

Faq savedFaq = faqRepository.save(faq);
FaqDTO dto = new FaqDTO(savedFaq);
Expand All @@ -99,13 +101,14 @@ public ResponseEntity<FaqDTO> createFaq(@RequestBody Faq faq, @PathVariable Long
* 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
@EnforceAtLeastEditor
public ResponseEntity<FaqDTO> updateFaq(@RequestBody Faq faq, @PathVariable Long faqId, @PathVariable Long courseId) {
log.debug("REST request to update Faq : {}", faq);
if (faqId == null || !faqId.equals(faq.getId())) {
throw new BadRequestAlertException("Id of FAQ and path must match", ENTITY_NAME, "idNull");
}
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null);
Course course = courseRepository.findByIdElseThrow(courseId);
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null);
Faq existingFaq = faqRepository.findByIdElseThrow(faqId);
if (!Objects.equals(existingFaq.getCourse().getId(), courseId)) {
throw new BadRequestAlertException("Course ID of the FAQ provided courseID must match", ENTITY_NAME, "idNull");
Expand Down Expand Up @@ -174,6 +177,34 @@ public ResponseEntity<Set<FaqDTO>> getFaqForCourse(@PathVariable Long courseId)
return ResponseEntity.ok().body(faqDTOS);
}

/**
* GET /courses/:courseId/faq-status/:faqState : get all the faqs of a course in the specified status
*
* @param courseId the courseId of the course for which all faqs should be returned
* @param faqState the state of all returned FAQs
* @return the ResponseEntity with status 200 (OK) and the list of faqs in body
*/
@GetMapping("courses/{courseId}/faq-state/{faqState}")
@EnforceAtLeastStudent
public ResponseEntity<Set<FaqDTO>> getAllFaqForCourseByStatus(@PathVariable Long courseId, @PathVariable String faqState) {
log.debug("REST request to get all Faqs for the course with id : {}", courseId);
FaqState retrivedState = defineState(faqState);
Course course = courseRepository.findByIdElseThrow(courseId);
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null);
Set<Faq> faqs = faqRepository.findAllByCourseIdAndFaqState(courseId, retrivedState);
Set<FaqDTO> faqDTOS = faqs.stream().map(FaqDTO::new).collect(Collectors.toSet());
return ResponseEntity.ok().body(faqDTOS);
}

private FaqState defineState(String faqState) {
return switch (faqState) {
case "ACCEPTED" -> FaqState.ACCEPTED;
case "REJECTED" -> FaqState.REJECTED;
case "PROPOSED" -> FaqState.PROPOSED;
default -> throw new IllegalArgumentException("Unknown state: " + faqState);
};
}

/**
* GET /courses/:courseId/faq-categories : get all the faq categories of a course
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
<span class="tab-link-text" jhiTranslate="entity.action.scores"></span>
</a>
}
@if (course.isAtLeastInstructor && course.faqEnabled) {
@if (course.isAtLeastEditor && course.faqEnabled) {
<a class="tab-link" [routerLink]="['/course-management', course.id, 'faqs']" routerLinkActive="active">
<fa-icon [icon]="faQuestion" />
<span class="tab-link-text" jhiTranslate="entity.action.faq"></span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ <h4 class="text-center no-exercises mt-3 fw-medium" jhiTranslate="artemisApp.cou
</a>
}

@if (course.isAtLeastInstructor && course.faqEnabled) {
@if (course.isAtLeastEditor && course.faqEnabled) {
<a
[routerLink]="['/course-management', course.id, 'faqs']"
class="btn btn-info me-1 mb-1"
Expand Down
6 changes: 3 additions & 3 deletions src/main/webapp/app/entities/faq.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { Course } from 'app/entities/course.model';
import { FaqCategory } from './faq-category.model';

export enum FaqState {
ACCEPTED,
REJECTED,
PROPOSED,
ACCEPTED = 'ACCEPTED',
REJECTED = 'REJECTED',
PROPOSED = 'PROPOSED',
}

export class Faq implements BaseEntity {
Expand Down
6 changes: 5 additions & 1 deletion src/main/webapp/app/faq/faq-update.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ 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 { AccountService } from 'app/core/auth/account.service';

@Component({
selector: 'jhi-faq-update',
Expand All @@ -31,6 +32,7 @@ export class FaqUpdateComponent implements OnInit {
existingCategories: FaqCategory[];
faqCategories: FaqCategory[];
courseId: number;
isAtleastInstructor: boolean = false;
domainActionsDescription = [new FormulaAction()];

// Icons
Expand All @@ -44,6 +46,7 @@ export class FaqUpdateComponent implements OnInit {
private navigationUtilService = inject(ArtemisNavigationUtilService);
private router = inject(Router);
private translateService = inject(TranslateService);
private accountService = inject(AccountService);

ngOnInit() {
this.isSaving = false;
Expand All @@ -56,6 +59,7 @@ export class FaqUpdateComponent implements OnInit {
if (course) {
this.faq.course = course;
this.loadCourseFaqCategories(course.id);
this.isAtleastInstructor = this.accountService.isAtLeastInstructorInCourse(course);
}
this.faqCategories = faq?.categories ? faq.categories : [];
});
Expand All @@ -77,10 +81,10 @@ export class FaqUpdateComponent implements OnInit {
*/
save() {
this.isSaving = true;
this.faq.faqState = this.isAtleastInstructor ? FaqState.ACCEPTED : FaqState.PROPOSED;
if (this.faq.id !== undefined) {
this.subscribeToSaveResponse(this.faqService.update(this.courseId, this.faq));
} else {
this.faq.faqState = FaqState.ACCEPTED;
this.subscribeToSaveResponse(this.faqService.create(this.courseId, this.faq));
}
}
Expand Down
62 changes: 44 additions & 18 deletions src/main/webapp/app/faq/faq.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,17 @@ <h2 id="page-heading">
}
</div>
<div class="d-flex-end justify-content-end">
<a id="create-faq" class="btn btn-primary mb-1" [routerLink]="['new']">
<fa-icon [icon]="faPlus" />
<span jhiTranslate="artemisApp.faq.home.createLabel"></span>
</a>
@if (isAtleastInstrucor) {
<a id="create-faq" class="btn btn-primary mb-1" [routerLink]="['new']">
<fa-icon [icon]="faPlus" />
<span jhiTranslate="artemisApp.faq.home.createLabel"></span>
</a>
} @else {
<a id="propose-faq" class="btn btn-primary mb-1" [routerLink]="['new']">
<fa-icon [icon]="faPlus" />
<span jhiTranslate="artemisApp.faq.home.proposeLabel"></span>
</a>
}
</div>
</div>
</div>
Expand All @@ -64,6 +71,10 @@ <h2 id="page-heading">
<span jhiTranslate="artemisApp.faq.table.questionAnswer"></span>
<fa-icon [icon]="faSort" />
</th>
<th jhiSortBy="faqState">
<span jhiTranslate="artemisApp.faq.table.state"></span>
<fa-icon [icon]="faSort" />
</th>
<th jhiSortBy="categories">
<span jhiTranslate="artemisApp.faq.table.categories"></span>
<fa-icon [icon]="faSort" />
Expand All @@ -83,34 +94,49 @@ <h2 id="page-heading">
<td>
<p class="markdown-preview" [innerHTML]="faq.questionAnswer | htmlForMarkdown"></p>
</td>
<td>
<p class="markdown-preview" [innerHTML]="faq.faqState | htmlForMarkdown"></p>
</td>
<td>
<div class="d-flex">
@for (category of faq.categories; track category) {
<jhi-custom-exercise-category-badge [category]="category" />
}
</div>
</td>

<td class="text-end">
<div class="btn-group flex-btn-group-container">
@if (faq.faqState == FaqState.PROPOSED && isAtleastInstrucor) {
<div class="btn-group-vertical me-1 mb-1">
<button class="btn btn-success btn-sm" (click)="acceptProposedFaq(courseId, faq)">
<fa-icon [icon]="faCheck" />
<span class="d-none d-md-inline" jhiTranslate="artemisApp.faq.home.accept"></span>
</button>
<button type="button" class="mt-1 btn btn-warning" id="reject-faq-{{ faq.id }}" (click)="rejectFaq(courseId, faq)">
<fa-icon [icon]="faCancel" />
<span class="d-none d-md-inline" jhiTranslate="artemisApp.faq.home.reject"></span>
</button>
</div>
}
<div class="btn-group-vertical me-1 mb-1">
<a [routerLink]="[faq.id, 'edit']" class="btn btn-primary btn-sm">
<fa-icon [icon]="faPencilAlt" />
<span class="d-none d-md-inline" jhiTranslate="entity.action.edit"></span>
</a>

<button
class="mt-1"
jhiDeleteButton
id="delete-faq-{{ faq.id }}"
[entityTitle]="faq.questionTitle || ''"
deleteQuestion="artemisApp.faq.delete.question"
deleteConfirmationText="artemisApp.faq.delete.typeNameToConfirm"
(delete)="deleteFaq(courseId, faq.id!)"
[dialogError]="dialogError$"
>
<fa-icon [icon]="faTrash" />
</button>
@if (isAtleastInstrucor) {
<button
class="mt-1"
jhiDeleteButton
id="delete-faq-{{ faq.id }}"
[entityTitle]="faq.questionTitle || ''"
deleteQuestion="artemisApp.faq.delete.question"
deleteConfirmationText="artemisApp.faq.delete.typeNameToConfirm"
(delete)="deleteFaq(courseId, faq.id!)"
[dialogError]="dialogError$"
>
<fa-icon [icon]="faTrash" />
</button>
}
</div>
</div>
</td>
Expand Down
40 changes: 38 additions & 2 deletions src/main/webapp/app/faq/faq.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
import { Faq } from 'app/entities/faq.model';
import { faEdit, faFilter, faPencilAlt, faPlus, faSort, faTrash } from '@fortawesome/free-solid-svg-icons';
import { Faq, FaqState } from 'app/entities/faq.model';
import { faCancel, faCheck, 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';
Expand All @@ -15,6 +15,9 @@ import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-catego
import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module';
import { ArtemisSharedModule } from 'app/shared/shared.module';
import { ArtemisMarkdownModule } from 'app/shared/markdown.module';
import { AccountService } from 'app/core/auth/account.service';
import { Course } from 'app/entities/course.model';
import { TranslateService } from '@ngx-translate/core';

@Component({
selector: 'jhi-faq',
Expand All @@ -24,11 +27,14 @@ import { ArtemisMarkdownModule } from 'app/shared/markdown.module';
imports: [ArtemisSharedModule, CustomExerciseCategoryBadgeComponent, ArtemisSharedComponentModule, ArtemisMarkdownModule],
})
export class FaqComponent implements OnInit, OnDestroy {
protected readonly FaqState = FaqState;
faqs: Faq[];
course: Course;
filteredFaqs: Faq[];
existingCategories: FaqCategory[];
courseId: number;
hasCategories: boolean = false;
isAtleastInstrucor: boolean = false;

private dialogErrorSource = new Subject<string>();
dialogError$ = this.dialogErrorSource.asObservable();
Expand All @@ -44,11 +50,15 @@ export class FaqComponent implements OnInit, OnDestroy {
faPencilAlt = faPencilAlt;
faFilter = faFilter;
faSort = faSort;
faCancel = faCancel;
faCheck = faCheck;

private faqService = inject(FaqService);
private route = inject(ActivatedRoute);
private alertService = inject(AlertService);
private sortService = inject(SortService);
private accountService = inject(AccountService);
private translateService = inject(TranslateService);

constructor() {
this.predicate = 'id';
Expand All @@ -59,6 +69,14 @@ export class FaqComponent implements OnInit, OnDestroy {
this.courseId = Number(this.route.snapshot.paramMap.get('courseId'));
this.loadAll();
this.loadCourseFaqCategories(this.courseId);
this.route.data.subscribe((data) => {
const course = data['course'];
if (course) {
this.course = course;
this.isAtleastInstrucor = this.accountService.isAtLeastInstructorInCourse(course);
console.log(this.isAtleastInstrucor);
}
});
}

ngOnDestroy(): void {
Expand Down Expand Up @@ -121,4 +139,22 @@ export class FaqComponent implements OnInit, OnDestroy {
});
this.applyFilters();
}

rejectFaq(courseId: number, faq: Faq) {
faq.faqState = FaqState.REJECTED;
faq.course = this.course;
this.faqService.update(courseId, faq).subscribe({
next: () => this.alertService.success(this.translateService.instant('artemisApp.faq.rejected', { id: faq.id })),
error: (error: HttpErrorResponse) => this.dialogErrorSource.next(error.message),
});
}

acceptProposedFaq(courseId: number, faq: Faq) {
faq.faqState = FaqState.ACCEPTED;
faq.course = this.course;
this.faqService.update(courseId, faq).subscribe({
next: () => this.alertService.success(this.translateService.instant('artemisApp.faq.accepted', { id: faq.id })),
error: (error: HttpErrorResponse) => this.dialogErrorSource.next(error.message),
});
}
}
14 changes: 14 additions & 0 deletions src/main/webapp/app/faq/faq.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ export class FaqService {
);
}

proposeFaq(courseId: number, faq: Faq) {
faq.faqState = FaqState.PROPOSED;
this.create(courseId, faq);
}

update(courseId: number, faq: Faq): Observable<EntityResponseType> {
console.log(faq);
const copy = FaqService.convertFaqFromClient(faq);
return this.http.put<Faq>(`${this.resourceUrl}/${courseId}/faqs/${faq.id}`, copy, { observe: 'response' }).pipe(
map((res: EntityResponseType) => {
Expand All @@ -47,6 +53,14 @@ export class FaqService {
.pipe(map((res: EntityArrayResponseType) => FaqService.convertFaqCategoryArrayFromServer(res)));
}

findAllByCourseIdAndState(courseId: number, faqState: FaqState): Observable<EntityArrayResponseType> {
return this.http
.get<Faq[]>(`${this.resourceUrl}/${courseId}/faq-state/${faqState}`, {
observe: 'response',
})
.pipe(map((res: EntityArrayResponseType) => FaqService.convertFaqCategoryArrayFromServer(res)));
}

delete(courseId: number, faqId: number): Observable<HttpResponse<any>> {
return this.http.delete<any>(`${this.resourceUrl}/${courseId}/faqs/${faqId}`, { observe: 'response' });
}
Expand Down
Loading

0 comments on commit 0544837

Please sign in to comment.