Skip to content

Commit

Permalink
Merge branch 'develop' into bugfix/iris/event-service-exception
Browse files Browse the repository at this point in the history
  • Loading branch information
kaancayli authored Jan 31, 2025
2 parents 7ffddc9 + 833e46c commit d3c25c6
Show file tree
Hide file tree
Showing 117 changed files with 3,709 additions and 2,295 deletions.
38 changes: 19 additions & 19 deletions gradle/jacoco.gradle
Original file line number Diff line number Diff line change
@@ -1,80 +1,80 @@
ext {
AggregatedCoverageThresholds = [
"INSTRUCTION": 0.895,
"INSTRUCTION": 0.89,
"CLASS": 56
];
// (Isolated) thresholds when executing each module on its own
ModuleCoverageThresholds = [
"assessment" : [
"INSTRUCTION": 0.779,
"INSTRUCTION": 0.77,
"CLASS": 8
],
"athena" : [
"INSTRUCTION": 0.856,
"INSTRUCTION": 0.85,
"CLASS": 2
],
"atlas" : [
"INSTRUCTION": 0.850,
"INSTRUCTION": 0.85,
"CLASS": 12
],
"buildagent" : [
"INSTRUCTION": 0.313,
"INSTRUCTION": 0.31,
"CLASS": 13
],
"communication": [
"INSTRUCTION": 0.890,
"INSTRUCTION": 0.89,
"CLASS": 7
],
"core" : [
"INSTRUCTION": 0.657,
"INSTRUCTION": 0.65,
"CLASS": 69
],
"exam" : [
"INSTRUCTION": 0.914,
"INSTRUCTION": 0.91,
"CLASS": 1
],
"exercise" : [
"INSTRUCTION": 0.649,
"INSTRUCTION": 0.64,
"CLASS": 9
],
"fileupload" : [
"INSTRUCTION": 0.906,
"INSTRUCTION": 0.90,
"CLASS": 1
],
"iris" : [
"INSTRUCTION": 0.760,
"INSTRUCTION": 0.74,
"CLASS": 25
],
"lecture" : [
"INSTRUCTION": 0.867,
"INSTRUCTION": 0.86,
"CLASS": 0
],
"lti" : [
"INSTRUCTION": 0.770,
"INSTRUCTION": 0.77,
"CLASS": 3
],
"modeling" : [
"INSTRUCTION": 0.891,
"INSTRUCTION": 0.89,
"CLASS": 2
],
"plagiarism" : [
"INSTRUCTION": 0.760,
"INSTRUCTION": 0.76,
"CLASS": 1
],
"programming" : [
"INSTRUCTION": 0.863,
"INSTRUCTION": 0.86,
"CLASS": 12
],
"quiz" : [
"INSTRUCTION": 0.784,
"INSTRUCTION": 0.78,
"CLASS": 6
],
"text" : [
"INSTRUCTION": 0.847,
"INSTRUCTION": 0.84,
"CLASS": 0
],
"tutorialgroup": [
"INSTRUCTION": 0.915,
"INSTRUCTION": 0.91,
"CLASS": 0
],
]
Expand Down
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@ module.exports = {
coverageThreshold: {
global: {
// TODO: in the future, the following values should increase to at least 90%
statements: 88.82,
branches: 74.45,
functions: 82.97,
lines: 88.84,
statements: 88.87,
branches: 74.51,
functions: 83.09,
lines: 88.89,
},
},
coverageReporters: ['clover', 'json', 'lcov', 'text-summary'],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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 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.domain.FaqState;
import de.tum.cit.aet.artemis.communication.repository.FaqRepository;
import de.tum.cit.aet.artemis.core.service.ProfileService;
import de.tum.cit.aet.artemis.iris.service.pyris.PyrisWebhookService;

@Profile(PROFILE_CORE)
@Service
public class FaqService {

private final Optional<PyrisWebhookService> pyrisWebhookService;

private final FaqRepository faqRepository;

public FaqService(FaqRepository faqRepository, Optional<PyrisWebhookService> pyrisWebhookService, ProfileService profileService) {

this.pyrisWebhookService = pyrisWebhookService;
this.faqRepository = faqRepository;

}

/**
* Ingests FAQs into the Pyris system. If a specific FAQ ID is provided, the method will attempt to add
* that FAQ to Pyris. Otherwise, it will ingest all FAQs for the specified course that are in the "ACCEPTED" state.
* If the PyrisWebhookService is unavailable, the method does nothing.
*
* @param courseId the ID of the course for which FAQs will be ingested
* @param faqId an optional ID of a specific FAQ to ingest; if not provided, all accepted FAQs for the course are processed
* @throws IllegalArgumentException if a specific FAQ is provided but its state is not "ACCEPTED"
*/
public void ingestFaqsIntoPyris(Long courseId, Optional<Long> faqId) {
if (pyrisWebhookService.isEmpty()) {
return;
}

faqId.ifPresentOrElse(id -> {
Faq faq = faqRepository.findById(id).orElseThrow();
if (faq.getFaqState() != FaqState.ACCEPTED) {
throw new IllegalArgumentException("Faq is not in the state accepted, you cannot ingest this faq");
}
pyrisWebhookService.get().addFaq(faq);
}, () -> faqRepository.findAllByCourseIdAndFaqState(courseId, FaqState.ACCEPTED).forEach(faq -> pyrisWebhookService.get().addFaq(faq)));
}

/**
* Deletes an existing FAQ from the Pyris system. If the PyrisWebhookService is unavailable, the method does nothing.
*
* @param existingFaq the FAQ to be removed from Pyris
*/
public void deleteFaqInPyris(Faq existingFaq) {
if (pyrisWebhookService.isEmpty()) {
return;
}

pyrisWebhookService.get().deleteFaq(existingFaq);
}

/**
* Automatically updates or ingests a specific FAQ into the Pyris system for a given course.
* If the PyrisWebhookService is unavailable, the method does nothing.
*
* @param courseId the ID of the course to which the FAQ belongs
* @param faq the FAQ to be ingested or updated in Pyris
*/
public void autoIngestFaqsIntoPyris(Long courseId, Faq faq) {
if (pyrisWebhookService.isEmpty()) {
return;
}

if (faq.getFaqState() != FaqState.ACCEPTED) {
return;
}

pyrisWebhookService.get().autoUpdateFaqInPyris(courseId, faq);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package de.tum.cit.aet.artemis.communication.web;

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

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

Expand All @@ -20,12 +22,14 @@
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.RequestParam;
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.communication.service.FaqService;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
Expand Down Expand Up @@ -58,10 +62,13 @@ public class FaqResource {

private final FaqRepository faqRepository;

public FaqResource(CourseRepository courseRepository, AuthorizationCheckService authCheckService, FaqRepository faqRepository) {
private final FaqService faqService;

public FaqResource(CourseRepository courseRepository, AuthorizationCheckService authCheckService, FaqRepository faqRepository, FaqService faqService) {
this.faqRepository = faqRepository;
this.courseRepository = courseRepository;
this.authCheckService = authCheckService;
this.faqService = faqService;
}

/**
Expand All @@ -86,6 +93,7 @@ public ResponseEntity<FaqDTO> createFaq(@RequestBody Faq faq, @PathVariable Long
}
Faq savedFaq = faqRepository.save(faq);
FaqDTO dto = new FaqDTO(savedFaq);
faqService.autoIngestFaqsIntoPyris(courseId, savedFaq);
return ResponseEntity.created(new URI("/api/courses/" + courseId + "/faqs/" + savedFaq.getId())).body(dto);
}

Expand All @@ -112,6 +120,7 @@ public ResponseEntity<FaqDTO> updateFaq(@RequestBody Faq faq, @PathVariable Long
throw new BadRequestAlertException("Course ID of the FAQ provided courseID must match", ENTITY_NAME, "idNull");
}
Faq updatedFaq = faqRepository.save(faq);
faqService.autoIngestFaqsIntoPyris(courseId, updatedFaq);
FaqDTO dto = new FaqDTO(updatedFaq);
return ResponseEntity.ok().body(dto);
}
Expand Down Expand Up @@ -152,6 +161,7 @@ public ResponseEntity<Void> deleteFaq(@PathVariable Long faqId, @PathVariable Lo
if (!Objects.equals(existingFaq.getCourse().getId(), courseId)) {
throw new BadRequestAlertException("Course ID of the FAQ provided courseID must match", ENTITY_NAME, "idNull");
}
faqService.deleteFaqInPyris(existingFaq);
faqRepository.deleteById(faqId);
return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, faqId.toString())).build();
}
Expand Down Expand Up @@ -212,6 +222,23 @@ public ResponseEntity<Set<String>> getFaqCategoriesForCourseByState(@PathVariabl
return ResponseEntity.ok().body(faqs);
}

/**
* POST /courses/{courseId}/ingest
* This endpoint is for starting the ingestion of all faqs or only one faq when triggered in Artemis.
*
* @param courseId the ID of the course for which all faqs should be ingested in pyris
* @param faqId If this id is present then only ingest this one faq of the respective course
* @return the ResponseEntity with status 200 (OK) and a message success or null if the operation failed
*/
@Profile(PROFILE_IRIS)
@PostMapping("courses/{courseId}/faqs/ingest")
@EnforceAtLeastInstructorInCourse
public ResponseEntity<Void> ingestFaqInIris(@PathVariable Long courseId, @RequestParam(required = false) Optional<Long> faqId) {
Course course = courseRepository.findByIdElseThrow(courseId);
faqService.ingestFaqsIntoPyris(courseId, faqId);
return ResponseEntity.ok().build();
}

/**
* @param courseId the id of the course the faq belongs to
* @param role the required role of the user
Expand All @@ -235,4 +262,8 @@ private void checkIsInstructorForAcceptedFaq(FaqState faqState, Long courseId) {
}
}

private boolean checkIfFaqIsAccepted(Faq faq) {
return faq.getFaqState() == FaqState.ACCEPTED;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,7 @@ public final class Constants {

public static final String INFO_SSH_KEYS_URL_DETAIL = "sshKeysURL";

public static final String INFO_VERSION_CONTROL_ACCESS_TOKEN_DETAIL = "useVersionControlAccessToken";

public static final String INFO_SHOW_CLONE_URL_WITHOUT_TOKEN = "showCloneUrlWithoutToken";
public static final String INFO_CODE_BUTTON_AUTHENTICATION_MECHANISMS = "authenticationMechanisms";

public static final String REGISTRATION_ENABLED = "registrationEnabled";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ public boolean isAeolusActive() {
return isProfileActive(Constants.PROFILE_AEOLUS);
}

/**
* Checks if the IRIS profile is active
*
* @return true if the aeolus profile is active, false otherwise
*/
public boolean isIrisActive() {
return isProfileActive(Constants.PROFILE_IRIS);
}

/**
* Checks if the lti profile is active
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,22 @@ public ResponseEntity<Void> resumeAllBuildAgents() {
localCIBuildJobQueueService.resumeAllBuildAgents();
return ResponseEntity.noContent().build();
}

/**
* {@code PUT /api/admin/clear-distributed-data} : Clear all distributed data.
* This endpoint allows administrators to clear all distributed data. See {@link SharedQueueManagementService#clearDistributedData()}.
*
* <p>
* <strong>Authorization:</strong> This operation requires admin privileges, enforced by {@code @EnforceAdmin}.
* </p>
*
* @return {@link ResponseEntity} with status code 200 (OK) if the distributed data was successfully cleared
* or an appropriate error response if something went wrong
*/
@DeleteMapping("clear-distributed-data")
public ResponseEntity<Void> clearDistributedData() {
log.debug("REST request to clear distributed data");
localCIBuildJobQueueService.clearDistributedData();
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public class IrisCourseSettings extends IrisSettings {
@JoinColumn(name = "iris_lecture_ingestion_settings_id")
private IrisLectureIngestionSubSettings irisLectureIngestionSettings;

@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
@JoinColumn(name = "iris_faq_ingestion_settings_id")
private IrisFaqIngestionSubSettings irisFaqIngestionSettings;

@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "iris_competency_generation_settings_id")
private IrisCompetencyGenerationSubSettings irisCompetencyGenerationSettings;
Expand Down Expand Up @@ -101,4 +105,14 @@ public IrisCompetencyGenerationSubSettings getIrisCompetencyGenerationSettings()
public void setIrisCompetencyGenerationSettings(IrisCompetencyGenerationSubSettings irisCompetencyGenerationSubSettings) {
this.irisCompetencyGenerationSettings = irisCompetencyGenerationSubSettings;
}

@Override
public IrisFaqIngestionSubSettings getIrisFaqIngestionSettings() {
return irisFaqIngestionSettings;
}

@Override
public void setIrisFaqIngestionSettings(IrisFaqIngestionSubSettings irisFaqIngestionSubSettings) {
this.irisFaqIngestionSettings = irisFaqIngestionSubSettings;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,16 @@ public IrisCompetencyGenerationSubSettings getIrisCompetencyGenerationSettings()
public void setIrisCompetencyGenerationSettings(IrisCompetencyGenerationSubSettings irisCompetencyGenerationSubSettings) {

}

@Override
public IrisFaqIngestionSubSettings getIrisFaqIngestionSettings() {
// Empty because exercises don't have exercise faq settings
return null;
}

@Override
public void setIrisFaqIngestionSettings(IrisFaqIngestionSubSettings irisFaqIngestionSubSettings) {
// Empty because exercises don't have exercise faq settings

}
}
Loading

0 comments on commit d3c25c6

Please sign in to comment.