SEBSERV-417 added get exam data from Moodle on exam import and fixed teacher account for different exams

This commit is contained in:
anhefti 2024-06-18 09:46:07 +02:00
parent b6e3772dcd
commit 6de875b29c
15 changed files with 110 additions and 24 deletions

View file

@ -176,6 +176,7 @@ public final class API {
public static final String LMS_FULL_INTEGRATION_COURSE_ID = "course_id";
public static final String LMS_FULL_INTEGRATION_QUIZ_ID = "quiz_id";
public static final String LMS_FULL_INTEGRATION_EXAM_TEMPLATE_ID = "exam_template_id";
public static final String LMS_FULL_INTEGRATION_EXAM_DATA = "exam_data";
public static final String LMS_FULL_INTEGRATION_QUIT_PASSWORD = "quit_password";
public static final String LMS_FULL_INTEGRATION_QUIT_LINK = "quit_link";
public static final String LMS_FULL_INTEGRATION_USER_ID = "user_id";

View file

@ -38,18 +38,17 @@ public interface TeacherAccountService {
final FullLmsIntegrationService.AdHocAccountData adHocAccountData) {
return getTeacherAccountIdentifier(
exam.getModelId(),
String.valueOf(exam.lmsSetupId),
adHocAccountData.userId);
}
/** Get the identifier for certain Teacher account for specified Exam.
*
* @param examId The Exam identifier
* @param lmsId The Lms identifier
* @param userId the account id
* @return account identifier
*/
String getTeacherAccountIdentifier(String examId, String lmsId, String userId);
String getTeacherAccountIdentifier(String lmsId, String userId);
/** Deactivates a certain ad-hoc Teacher account
* Usually called when an exam is deleted. Checks if Teacher account for exam

View file

@ -78,15 +78,14 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
@Override
public String getTeacherAccountIdentifier(
final String examId,
final String lmsId,
final String userId) {
if (examId == null || lmsId == null || userId == null) {
if (lmsId == null || userId == null) {
throw new RuntimeException("examId and/or userId cannot be null");
}
return examId + Constants.UNDERLINE + lmsId + Constants.UNDERLINE + userId;
return "TEACHER_" + Constants.UNDERLINE + lmsId + Constants.UNDERLINE + userId;
}
@Override

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
@ -32,4 +33,5 @@ public interface FullLmsIntegrationAPI {
Result<String> deleteConnectionDetails();
Result<QuizData> getQuizDataForRemoteImport(String examData);
}

View file

@ -53,7 +53,8 @@ public interface FullLmsIntegrationService {
String quizId,
String examTemplateId,
String quitPassword,
String quitLink);
String quitLink,
final String examData);
Result<EntityKey> deleteExam(
String lmsUUID,

View file

@ -315,12 +315,14 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
final String quizId,
final String examTemplateId,
final String quitPassword,
final String quitLink) {
final String quitLink,
final String examData) {
return lmsSetupDAO
.getLmsSetupIdByConnectionId(lmsUUID)
.flatMap(lmsAPITemplateCacheService::getLmsAPITemplate)
.map(findQuizData(courseId, quizId))
.map(template -> getQuizData(template, courseId, quizId, examData))
//.map(findQuizData(courseId, quizId))
.map(createExam(examTemplateId, quitPassword))
.map(exam -> applyExamData(exam, false))
.map(this::applyConnectionConfiguration);
@ -418,6 +420,22 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
.getOneTimeTokenForTeacherAccount(exam, adHocAccountData, true));
}
private QuizData getQuizData(
final LmsAPITemplate lmsAPITemplate,
final String courseId,
final String quizId,
final String examData) {
final String internalQuizId = MoodleUtils.getInternalQuizId(
quizId,
courseId,
null,
null);
return lmsAPITemplate.getQuizDataForRemoteImport(examData)
.getOrThrow();
}
private Function<LmsAPITemplate, QuizData> findQuizData(
final String courseId,

View file

@ -577,4 +577,14 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
.getOrThrow());
}
@Override
public Result<QuizData> getQuizDataForRemoteImport(final String examData) {
if (this.lmsIntegrationAPI == null) {
return Result.ofError(
new UnsupportedOperationException("LMS Integration API Not Supported For: " + getType().name()));
}
return this.lmsIntegrationAPI.getQuizDataForRemoteImport(examData);
}
}

View file

@ -447,6 +447,11 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
return Result.ofRuntimeError("Not Supported");
}
@Override
public Result<QuizData> getQuizDataForRemoteImport(final String examData) {
return Result.ofRuntimeError("Not Supported");
}
private enum LinkRel {
FIRST, LAST, PREV, NEXT
}

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
@ -42,4 +43,9 @@ public class MockupFullIntegration implements FullLmsIntegrationAPI {
public Result<String> deleteConnectionDetails() {
return Result.ofRuntimeError("TODO");
}
@Override
public Result<QuizData> getQuizDataForRemoteImport(final String examData) {
return Result.ofRuntimeError("Not Supported");
}
}

View file

@ -291,7 +291,7 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
return this.jsonMapper.readValue(
courseJSON,
Courses.class).courses
CoursesPlugin.class).results
.stream()
.flatMap(c -> MoodleUtils.quizDataOf(
lmsSetup,

View file

@ -8,14 +8,13 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
@ -30,6 +29,7 @@ import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@ -49,12 +49,19 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI {
private final JSONMapper jsonMapper;
private final MoodleRestTemplateFactory restTemplateFactory;
private final boolean prependShortCourseName;
public MoodlePluginFullIntegration(
final JSONMapper jsonMapper,
final MoodleRestTemplateFactory restTemplateFactory) {
final MoodleRestTemplateFactory restTemplateFactory,
final Environment environment) {
this.jsonMapper = jsonMapper;
this.restTemplateFactory = restTemplateFactory;
this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty(
"sebserver.webservice.lms.moodle.prependShortCourseName",
Constants.TRUE_STRING));
}
@Override
@ -289,6 +296,33 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI {
});
}
@Override
public Result<QuizData> getQuizDataForRemoteImport(final String examData) {
return Result.tryCatch(() -> {
log.info("****** Try to parse import exam data sent by Moodle on Exam import: {}", examData);
final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup();
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? lmsSetup.lmsApiUrl + MoodlePluginCourseAccess.MOODLE_QUIZ_START_URL_PATH
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MoodlePluginCourseAccess.MOODLE_QUIZ_START_URL_PATH;
MoodleUtils.checkJSONFormat(examData);
final MoodleUtils.CoursesPlugin courses = this.jsonMapper.readValue(
examData,
MoodleUtils.CoursesPlugin.class);
final MoodleUtils.CourseData courseData = courses.results.iterator().next();
final List<QuizData> quizData = MoodleUtils.quizDataOf(
lmsSetup,
courseData,
urlPrefix,
prependShortCourseName
);
return quizData.get(0);
});
}
private Result<MoodleAPIRestTemplate> getRestTemplate() {
final Result<MoodleAPIRestTemplate> result = this.restTemplateFactory.getRestTemplate();

View file

@ -103,7 +103,8 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
final MoodlePluginFullIntegration moodlePluginFullIntegration = new MoodlePluginFullIntegration(
this.jsonMapper,
moodleRestTemplateFactory
moodleRestTemplateFactory,
environment
);
return new LmsAPITemplateAdapter(

View file

@ -434,6 +434,11 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
return Result.ofRuntimeError("Not Supported");
}
@Override
public Result<QuizData> getQuizDataForRemoteImport(final String examData) {
return Result.ofRuntimeError("Not Supported");
}
private <T> T apiGet(final RestTemplate restTemplate, final String url, final Class<T> type) {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final ResponseEntity<T> res = restTemplate.exchange(

View file

@ -485,11 +485,14 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
}
private Result<Exam> deleteForExam(final Long examId) {
return this.examDAO
.byPK(examId)
.flatMap(this.screenProctoringAPIBinding::deactivateScreenProctoring)
.map(this::cleanupAllLocalGroups)
.onError(error -> log.error("Failed to delete SPS integration for exam: {}", examId, error));
return Result.tryCatch(() -> {
final Exam exam = this.examDAO.byPK(examId).getOrThrow();
this.screenProctoringAPIBinding.deactivateScreenProctoring(exam)
.onError(error -> log.error("Failed to deactivate screen proctoring for exam: {}", exam.name, error));
return this.cleanupAllLocalGroups(exam);
});
}
private Exam cleanupAllLocalGroups(final Exam exam) {

View file

@ -59,6 +59,7 @@ public class LmsIntegrationController {
@RequestParam(name = API.LMS_FULL_INTEGRATION_COURSE_ID) final String courseId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIZ_ID) final String quizId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_EXAM_TEMPLATE_ID) final String templateId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_EXAM_DATA, required = false) final String examData,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIT_PASSWORD, required = false) final String quitPassword,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIT_LINK, required = false) final String quitLink,
final HttpServletResponse response) {
@ -69,10 +70,11 @@ public class LmsIntegrationController {
quizId,
templateId,
quitPassword,
quitLink)
quitLink,
examData)
.onError(e -> log.error(
"Failed to create/import exam: lmsId:{}, courseId: {}, quizId: {}, templateId: {}",
lmsUUId, courseId, quizId, templateId, e))
"Failed to create/import exam: lmsId:{}, courseId: {}, quizId: {}, templateId: {} error: {}",
lmsUUId, courseId, quizId, templateId, e.getMessage()))
.getOrThrow();
log.info("Auto import of exam successful: {}", exam);