SEBSERV-75 implemented user account display name resolving

This commit is contained in:
anhefti 2020-07-15 12:32:26 +02:00
parent 7f6c3b46d6
commit 99f34176f2
11 changed files with 252 additions and 9 deletions

View file

@ -8,15 +8,20 @@
package ch.ethz.seb.sebserver.gbl.model.user;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.util.Utils;
public class ExamineeAccountDetails {
public static final String ATTR_ID = "id";
public static final String ATTR_NAME = "name";
public static final String ATTR_USER_NAME = "username";
public static final String ATTR_EMAIL = "email";
public static final String ATTR_ADDITIONAL_ATTRIBUTES = "additionalAttributes";
@JsonProperty(ATTR_ID)
public final String id;
@ -30,17 +35,22 @@ public class ExamineeAccountDetails {
@JsonProperty(ATTR_EMAIL)
public final String email;
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES)
public final Map<String, String> additionalAttributes;
@JsonCreator
public ExamineeAccountDetails(
@JsonProperty(ATTR_ID) final String id,
@JsonProperty(ATTR_NAME) final String name,
@JsonProperty(ATTR_USER_NAME) final String username,
@JsonProperty(ATTR_EMAIL) final String email) {
@JsonProperty(ATTR_EMAIL) final String email,
@JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES) final Map<String, String> additionalAttributes) {
this.id = id;
this.name = name;
this.username = username;
this.email = email;
this.additionalAttributes = Utils.immutableMapOf(additionalAttributes);
}
public String getId() {
@ -59,6 +69,17 @@ public class ExamineeAccountDetails {
return this.email;
}
public Map<String, String> getAdditionalAttributes() {
return this.additionalAttributes;
}
public String getDisplayName() {
if (this.name == null) {
return this.id;
}
return (this.name.equals(this.id)) ? this.id : this.name + " (" + this.id + ")";
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
@ -70,6 +91,8 @@ public class ExamineeAccountDetails {
builder.append(this.username);
builder.append(", email=");
builder.append(this.email);
builder.append(", additionalAttributes=");
builder.append(this.additionalAttributes);
builder.append("]");
return builder.toString();
}

View file

@ -25,6 +25,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException;
@ -105,8 +106,22 @@ public interface LmsAPITemplate {
Result<QuizData> getQuizFromCache(String id);
// TODO
//Result<ExamineeAccountDetails> getExamineeAccountDetails(String examineeUserId);
/** Convert a an anonymous or temporary user session identifier from SEB Client into a user
* account details.
*
* @param examineeSessionId the user session identifier from SEB Client
* @return a Result refer to the ExamineeAccountDetails instance or to an error when happened or not supported */
Result<ExamineeAccountDetails> getExamineeAccountDetails(String examineeSessionId);
/** Used to convert an anonymous or temporary user session identifier from SEB Client into a user
* account name for displaying on monitoring page.
*
* If the underling concrete template implementation does not support this user name conversion,
* the given examineeSessionId shall be returned.
*
* @param examineeSessionId the user session identifier from SEB Client
* @return a user account display name if supported or the given examineeSessionId if not. */
String getExamineeName(String examineeSessionId);
/** Used to get a list of chapters (display name and chapter-identifier) that can be used to
* apply chapter-based SEB restriction for a specified course.

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
@ -23,6 +24,7 @@ import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
import ch.ethz.seb.sebserver.gbl.async.MemoizingCircuitBreaker;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
@ -31,6 +33,7 @@ public abstract class CourseAccess {
protected final MemoizingCircuitBreaker<List<QuizData>> allQuizzesRequest;
protected final CircuitBreaker<Chapters> chaptersRequest;
protected final CircuitBreaker<ExamineeAccountDetails> accountDetailRequest;
protected CourseAccess(final AsyncService asyncService) {
this.allQuizzesRequest = asyncService.createMemoizingCircuitBreaker(
@ -45,6 +48,11 @@ public abstract class CourseAccess {
3,
Constants.MINUTE_IN_MILLIS,
Constants.MINUTE_IN_MILLIS);
this.accountDetailRequest = asyncService.createCircuitBreaker(
1,
Constants.SECOND_IN_MILLIS * 10,
Constants.SECOND_IN_MILLIS * 10);
}
public Result<QuizData> getQuizFromCache(final String id) {
@ -88,10 +96,34 @@ public abstract class CourseAccess {
.map(LmsAPIService.quizzesFilterFunction(filterMap));
}
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return this.accountDetailRequest.protectedRun(accountDetailsSupplier(examineeSessionId));
}
public String getExamineeName(final String examineeSessionId) {
return getExamineeAccountDetails(examineeSessionId)
.map(ExamineeAccountDetails::getDisplayName)
.getOr(examineeSessionId);
}
protected Result<Chapters> getCourseChapters(final String courseId) {
return this.chaptersRequest.protectedRun(getCourseChaptersSupplier(courseId));
}
/** NOTE: this returns a ExamineeAccountDetails with given examineeSessionId for default.
* Override this if requesting account details is supported for specified LMS access.
*
* @param examineeSessionId
* @return this returns a ExamineeAccountDetails with given examineeSessionId for default */
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String examineeSessionId) {
return () -> new ExamineeAccountDetails(
examineeSessionId,
examineeSessionId,
examineeSessionId,
examineeSessionId,
Collections.emptyMap());
}
protected abstract Supplier<List<QuizData>> allQuizzesSupplier();
protected abstract Supplier<Chapters> getCourseChaptersSupplier(final String courseId);

View file

@ -31,6 +31,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
@ -183,6 +184,16 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
return Result.ofError(new UnsupportedOperationException());
}
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return Result.ofError(new UnsupportedOperationException());
}
@Override
public String getExamineeName(final String examineeSessionId) {
return "--" + " (" + examineeSessionId + ")";
}
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
log.info("Apply SEB Client restriction for Exam: {}", exam);

View file

@ -24,6 +24,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
@ -86,6 +87,16 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
.get());
}
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return this.openEdxCourseAccess.getExamineeAccountDetails(examineeSessionId);
}
@Override
public String getExamineeName(final String examineeSessionId) {
return this.openEdxCourseAccess.getExamineeName(examineeSessionId);
}
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
if (log.isDebugEnabled()) {

View file

@ -20,6 +20,7 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@ -32,6 +33,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
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.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.CourseAccess;
@ -46,6 +48,7 @@ public class MoodleCourseAccess extends CourseAccess {
private static final String MOODLE_QUIZ_START_URL_PATH = "mod/quiz/view.php?id=";
private static final String MOODLE_COURSE_API_FUNCTION_NAME = "core_course_get_courses";
private static final String MOODLE_USER_PROFILE_API_FUNCTION_NAME = "core_user_get_users_by_field";
private static final String MOODLE_QUIZ_API_FUNCTION_NAME = "mod_quiz_get_quizzes_by_courses";
private static final String MOODLE_COURSE_API_COURSE_IDS = "courseids";
@ -55,6 +58,53 @@ public class MoodleCourseAccess extends CourseAccess {
private MoodleAPIRestTemplate restTemplate;
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return Result.tryCatch(() -> {
final MoodleAPIRestTemplate template = getRestTemplate()
.getOrThrow();
final MultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>();
queryAttributes.add("field", "id");
queryAttributes.add("values[0]", examineeSessionId);
final String userDetailsJSON = template.callMoodleAPIFunction(
MOODLE_USER_PROFILE_API_FUNCTION_NAME,
queryAttributes);
final MoodleUserDetails[] userDetails = this.jsonMapper.<MoodleUserDetails[]> readValue(
userDetailsJSON,
new TypeReference<MoodleUserDetails[]>() {
});
if (userDetails == null || userDetails.length <= 0) {
throw new RuntimeException("No user details on Moodle API request");
}
final Map<String, String> additionalAttributes = new HashMap<>();
additionalAttributes.put("firstname", userDetails[0].firstname);
additionalAttributes.put("lastname", userDetails[0].lastname);
additionalAttributes.put("department", userDetails[0].department);
additionalAttributes.put("firstaccess", String.valueOf(userDetails[0].firstaccess));
additionalAttributes.put("lastaccess", String.valueOf(userDetails[0].lastaccess));
additionalAttributes.put("auth", userDetails[0].auth);
additionalAttributes.put("suspended", String.valueOf(userDetails[0].suspended));
additionalAttributes.put("confirmed", String.valueOf(userDetails[0].confirmed));
additionalAttributes.put("lang", userDetails[0].lang);
additionalAttributes.put("theme", userDetails[0].theme);
additionalAttributes.put("timezone", userDetails[0].timezone);
additionalAttributes.put("description", userDetails[0].description);
additionalAttributes.put("mailformat", String.valueOf(userDetails[0].mailformat));
additionalAttributes.put("descriptionformat", String.valueOf(userDetails[0].descriptionformat));
return new ExamineeAccountDetails(
userDetails[0].id,
userDetails[0].fullname,
userDetails[0].username,
userDetails[0].email,
additionalAttributes);
});
}
protected MoodleCourseAccess(
final JSONMapper jsonMapper,
final LmsSetup lmsSetup,
@ -261,7 +311,6 @@ public class MoodleCourseAccess extends CourseAccess {
@JsonCreator
protected CourseQuizData(
@JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes) {
this.quizzes = quizzes;
}
}
@ -291,7 +340,69 @@ public class MoodleCourseAccess extends CourseAccess {
this.intro = intro;
this.time_limit = time_limit;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static final class MoodleUserDetails {
final String id;
final String username;
final String firstname;
final String lastname;
final String fullname;
final String email;
final String department;
final Long firstaccess;
final Long lastaccess;
final String auth;
final Boolean suspended;
final Boolean confirmed;
final String lang;
final String theme;
final String timezone;
final String description;
final Integer mailformat;
final Integer descriptionformat;
@JsonCreator
protected MoodleUserDetails(
@JsonProperty(value = "id") final String id,
@JsonProperty(value = "username") final String username,
@JsonProperty(value = "firstname") final String firstname,
@JsonProperty(value = "lastname") final String lastname,
@JsonProperty(value = "fullname") final String fullname,
@JsonProperty(value = "email") final String email,
@JsonProperty(value = "department") final String department,
@JsonProperty(value = "firstaccess") final Long firstaccess,
@JsonProperty(value = "lastaccess") final Long lastaccess,
@JsonProperty(value = "auth") final String auth,
@JsonProperty(value = "suspended") final Boolean suspended,
@JsonProperty(value = "confirmed") final Boolean confirmed,
@JsonProperty(value = "lang") final String lang,
@JsonProperty(value = "theme") final String theme,
@JsonProperty(value = "timezone") final String timezone,
@JsonProperty(value = "description") final String description,
@JsonProperty(value = "mailformat") final Integer mailformat,
@JsonProperty(value = "descriptionformat") final Integer descriptionformat) {
this.id = id;
this.username = username;
this.firstname = firstname;
this.lastname = lastname;
this.fullname = fullname;
this.email = email;
this.department = department;
this.firstaccess = firstaccess;
this.lastaccess = lastaccess;
this.auth = auth;
this.suspended = suspended;
this.confirmed = confirmed;
this.lang = lang;
this.theme = theme;
this.timezone = timezone;
this.description = description;
this.mailformat = mailformat;
this.descriptionformat = descriptionformat;
}
}
}

View file

@ -18,6 +18,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
@ -74,6 +75,16 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate {
.get());
}
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return this.moodleCourseAccess.getExamineeAccountDetails(examineeSessionId);
}
@Override
public String getExamineeName(final String examineeSessionId) {
return this.moodleCourseAccess.getExamineeName(examineeSessionId);
}
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
return Result.ofError(new UnsupportedOperationException("SEB Restriction API not available yet"));

View file

@ -24,6 +24,7 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCacheService;
/** A Service to handle running exam sessions */
@ -49,6 +50,11 @@ public interface ExamSessionService {
* @return the underling CacheManager */
CacheManager getCacheManager();
/** Get the underling LmsAPIService
*
* @return the underling LmsAPIService */
LmsAPIService getLmsAPIService();
/** Use this to check the consistency of a running Exam.
* Current consistency checks are:
* - Check if there is at least one Exam supporter attached to the Exam

View file

@ -100,6 +100,11 @@ public class ExamSessionServiceImpl implements ExamSessionService {
return this.cacheManager;
}
@Override
public LmsAPIService getLmsAPIService() {
return this.lmsAPIService;
}
@Override
public Result<Collection<APIMessage>> checkRunningExamConsistency(final Long examId) {
return Result.tryCatch(() -> {

View file

@ -265,7 +265,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
ClientConnection clientConnection = getClientConnection(connectionToken);
checkInstitutionalIntegrity(institutionId, clientConnection);
checkExamIntegrity(examId, clientConnection);
clientConnection = updateUserSessionId(userSessionId, clientConnection);
clientConnection = updateUserSessionId(userSessionId, clientConnection, examId);
// connection integrity check
if (clientConnection.status == ConnectionStatus.CONNECTION_REQUESTED) {
@ -292,7 +292,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
(examId != null) ? examId : clientConnection.examId,
ConnectionStatus.ACTIVE,
null,
userSessionId,
clientConnection.userSessionId,
null,
virtualClientAddress,
null);
@ -564,7 +564,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
checkExamRunning(examId);
}
private ClientConnection updateUserSessionId(final String userSessionId, ClientConnection clientConnection) {
private ClientConnection updateUserSessionId(
final String userSessionId,
ClientConnection clientConnection,
final Long examId) {
if (StringUtils.isNoneBlank(userSessionId)) {
if (StringUtils.isNoneBlank(clientConnection.userSessionId)) {
log.error(
@ -574,6 +578,20 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
"ClientConnection integrity violation: clientConnection has already a userSessionId");
}
// try to get user account display name
String accountId = userSessionId;
try {
accountId = this.examSessionService
.getRunningExam((clientConnection.examId != null)
? clientConnection.examId
: examId)
.flatMap(exam -> this.examSessionService.getLmsAPIService().getLmsAPITemplate(exam.lmsSetupId))
.map(template -> template.getExamineeName(userSessionId))
.getOr(userSessionId);
} catch (final Exception e) {
log.warn("Unexpected error while trying to get user account display name: {}", e.getMessage());
}
// create new ClientConnection for update
final ClientConnection authenticatedClientConnection = new ClientConnection(
clientConnection.id,
@ -581,7 +599,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
null,
ConnectionStatus.AUTHENTICATED,
null,
userSessionId,
accountId,
null,
null,
null);

View file

@ -1,6 +1,6 @@
spring.application.name=SEB Server
spring.profiles.active=ws,gui,dev
sebserver.version=1.0.1
sebserver.version=@sebserver-version@
##########################################################
### Global Server Settings