Skip to content

Commit

Permalink
add manual mapping possibility and unfinishedGradings
Browse files Browse the repository at this point in the history
  • Loading branch information
julianlxs committed Dec 23, 2024
1 parent 217e4b0 commit 32c3d1a
Show file tree
Hide file tree
Showing 10 changed files with 278 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ public List<ExternalAssignment> getExternalAssignments(@Argument final UUID cour
return gradingService.getExternalAssignments(courseId, currentUser);
}

@QueryMapping
public List<ManualMappingInstance> getManualMappingInstances(@Argument final UUID courseId, @ContextValue final LoggedInUser currentUser) {
return gradingService.getManualMappingInstances(courseId, currentUser);
}

/* Mutation Mappings */

@MutationMapping(name = "_internal_noauth_createAssignment")
Expand All @@ -72,6 +77,13 @@ public AssignmentCompletedFeedback logAssignmentCompleted(@Argument LogAssignmen
return assignmentService.logAssignmentCompleted(input, currentUser);
}

@MutationMapping
public List<String> saveStudentMappings(@Argument final UUID courseId,
@Argument final List<StudentMappingInput> studentMappingInputs,
@ContextValue final LoggedInUser currentUser) {
return gradingService.saveStudentMappings(courseId, studentMappingInputs, currentUser);
}

/* Schema Mappings */

@SchemaMapping(typeName = "AssignmentMutation")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package de.unistuttgart.iste.meitrex.assignment_service.persistence.entity;

import de.unistuttgart.iste.meitrex.common.persistence.IWithId;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.json.JSONObject;

@Entity(name = "ManualMappingInstance")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ManualMappingInstanceEntity implements IWithId<String> {

@Id
private String externalStudentId;

@Column(nullable = false)
private String externalStudentInfo;

@Override
public String getId() {
return externalStudentId;
}

public static ManualMappingInstanceEntity fromJson(JSONObject externalStudentInfo) {
return new ManualMappingInstanceEntity(externalStudentInfo.getString("id"), externalStudentInfo.toString());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package de.unistuttgart.iste.meitrex.assignment_service.persistence.entity;

import de.unistuttgart.iste.meitrex.common.persistence.IWithId;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.json.JSONObject;

import java.io.Serializable;
import java.util.UUID;

@Entity(name = "UnfinishedGrading")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UnfinishedGradingEntity implements IWithId<UnfinishedGradingEntity.PrimaryKey> {

@EmbeddedId
private PrimaryKey primaryKey;

@Column(nullable = false)
private String gradingJson;

@Column(nullable = false)
private int numberOfTries;

@Data
@Embeddable
@NoArgsConstructor
@AllArgsConstructor
public static class PrimaryKey implements Serializable {
private String externalStudentId;
private UUID assignmentId;
}

@Override
public PrimaryKey getId() {
return primaryKey;
}

public static UnfinishedGradingEntity fromJson(JSONObject gradingJson, UUID assignmentId) {
String studentId = gradingJson.getString("studentId");
return new UnfinishedGradingEntity(new PrimaryKey(studentId, assignmentId), gradingJson.toString(), 0);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,12 @@ public SubexerciseGrading subexerciseGradingEntityToDto(final SubexerciseGrading
return mappedSubexerciseGrading;
}


public StudentMappingEntity studentMappingInputToEntity(final StudentMappingInput studentMappingInput) {
return modelMapper.map(studentMappingInput, StudentMappingEntity.class);
}

public ManualMappingInstance manualMappingInstanceEntityToDto(final ManualMappingInstanceEntity manualMappingInstanceEntity) {
return modelMapper.map(manualMappingInstanceEntity, ManualMappingInstance.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package de.unistuttgart.iste.meitrex.assignment_service.persistence.repository;

import de.unistuttgart.iste.meitrex.assignment_service.persistence.entity.ManualMappingInstanceEntity;
import de.unistuttgart.iste.meitrex.common.persistence.MeitrexRepository;
import org.springframework.stereotype.Repository;


@Repository
public interface ManualMappingInstanceRepository extends MeitrexRepository<ManualMappingInstanceEntity, String> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package de.unistuttgart.iste.meitrex.assignment_service.persistence.repository;

import de.unistuttgart.iste.meitrex.assignment_service.persistence.entity.UnfinishedGradingEntity;
import de.unistuttgart.iste.meitrex.common.persistence.MeitrexRepository;
import org.springframework.stereotype.Repository;


@Repository
public interface UnfinishedGradingRepository extends MeitrexRepository<UnfinishedGradingEntity, UnfinishedGradingEntity.PrimaryKey> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import de.unistuttgart.iste.meitrex.assignment_service.persistence.entity.*;
import de.unistuttgart.iste.meitrex.assignment_service.persistence.mapper.AssignmentMapper;
import de.unistuttgart.iste.meitrex.assignment_service.persistence.repository.GradingRepository;
import de.unistuttgart.iste.meitrex.assignment_service.persistence.repository.ManualMappingInstanceRepository;
import de.unistuttgart.iste.meitrex.assignment_service.persistence.repository.StudentMappingRepository;
import de.unistuttgart.iste.meitrex.assignment_service.persistence.repository.UnfinishedGradingRepository;
import de.unistuttgart.iste.meitrex.assignment_service.validation.AssignmentValidator;
import de.unistuttgart.iste.meitrex.common.dapr.TopicPublisher;
import de.unistuttgart.iste.meitrex.common.event.ContentProgressedEvent;
Expand Down Expand Up @@ -42,6 +44,8 @@ public class GradingService {
private final TopicPublisher topicPublisher;
private final AssignmentService assignmentService;
private final StudentMappingRepository studentMappingRepository;
private final ManualMappingInstanceRepository manualMappingInstanceRepository;
private final UnfinishedGradingRepository unfinishedGradingRepository;

private final GraphQlClient userServiceClient;

Expand Down Expand Up @@ -110,9 +114,12 @@ private List<GradingEntity> parseStringIntoGradingEntityList(final String string
final List<GradingEntity> gradingEntityList = new ArrayList<>(gradingArray.length());
GradingEntity gradingEntity;
for (int i = 0; i < gradingArray.length(); i++) {
gradingEntity = parseIntoGradingEntity(gradingArray.getJSONObject(i), assignmentEntity);
if (gradingEntity != null) {
JSONObject jsonObject = gradingArray.getJSONObject(i);
try {
gradingEntity = parseIntoGradingEntity(jsonObject, assignmentEntity);
gradingEntityList.add(gradingEntity);
} catch (ManualMappingRequiredException e) {
// fine, will be handled by manual mapping of admin
}
}
return gradingEntityList;
Expand All @@ -125,16 +132,30 @@ private List<GradingEntity> parseStringIntoGradingEntityList(final String string
* @param assignmentEntity the assignment id which the grading belongs to
* @return parsed grading entity
*/
private GradingEntity parseIntoGradingEntity(final JSONObject jsonObject, final AssignmentEntity assignmentEntity) {
private GradingEntity parseIntoGradingEntity(final JSONObject jsonObject, final AssignmentEntity assignmentEntity) throws ManualMappingRequiredException {
final GradingEntity gradingEntity = new GradingEntity();

String externalStudentId = jsonObject.getString("studentId"); // TODO match this to Meitrex student id
UUID studentId;
try {
studentId = getStudentIdFromExternalStudentId(externalStudentId);
} catch (ManualMappingRequiredException e) {
manualMappingInstanceRepository.save(e.getExternalStudentInfo());
return null;
// ManualMappingInstance is added to repository, so that an admin can map manually
JSONObject externalStudentInfo = e.getExternalStudentInfo();
manualMappingInstanceRepository.save(ManualMappingInstanceEntity.fromJson(externalStudentInfo));

// Grading is added to unfinished grading repository, so that it can be tried again, when a manual mapping was done.
UnfinishedGradingEntity unfinishedGradingEntity;
Optional<UnfinishedGradingEntity> foundEntityOptional = unfinishedGradingRepository.findById(new UnfinishedGradingEntity.PrimaryKey(externalStudentId, assignmentEntity.getAssessmentId()));
if (foundEntityOptional.isPresent()) {
unfinishedGradingEntity = foundEntityOptional.get();
unfinishedGradingEntity.setNumberOfTries(unfinishedGradingEntity.getNumberOfTries() + 1);
} else {
unfinishedGradingEntity = UnfinishedGradingEntity.fromJson(jsonObject, assignmentEntity.getAssessmentId());
}
unfinishedGradingRepository.save(unfinishedGradingEntity);

throw(e);
}

JSONObject gradingData = jsonObject.getJSONObject("gradingData");
Expand Down Expand Up @@ -244,7 +265,7 @@ private UUID getStudentIdFromExternalStudentId(final String externalStudentId) t
if (studentMappingEntity.isPresent()) {
return studentMappingEntity.get().getMeitrexStudentId();
}
UUID newMeitrexStudentId = findNewStudentIdFromExternalStudentId(externalStudentId);
UUID newMeitrexStudentId = findNewStudentIdFromExternalStudentId(externalStudentId); // throws exception if nothing is found
studentMappingRepository.save(new StudentMappingEntity(externalStudentId, newMeitrexStudentId));
return newMeitrexStudentId;
}
Expand Down Expand Up @@ -276,9 +297,9 @@ private UUID findNewStudentIdFromExternalStudentId(final String externalStudentI
return (UUID) filteredByFirstName.getFirst().get("id");
}

// filter by more attributes like email, matriculation number etc if there are still more candidates
// filter by more attributes like email, matriculation number etc. if there are still more candidates

// create possibility for manual user mapping
// if no match is found, the id needs to be mapped manually
throw new ManualMappingRequiredException(externalStudentInfo);
}

Expand Down Expand Up @@ -338,6 +359,57 @@ private void handleGraphQlResponse(final ClientGraphQlResponse result, final Syn
sink.next(retrievedUserInfos);
}

public List<String> saveStudentMappings(final UUID courseId, final List<StudentMappingInput> studentMappingInputs, final LoggedInUser currentUser) {
try {
validateUserHasAccessToCourse(currentUser, LoggedInUser.UserRoleInCourse.ADMINISTRATOR, courseId);
} catch (final NoAccessToCourseException ex) {
return null;
}

// deletes all the external ids that have just been mapped from the manualMappingInstance-repo
List<String> externalStudentIdList = studentMappingInputs.stream().map(StudentMappingInput::getExternalStudentId).toList();
manualMappingInstanceRepository.deleteAllById(externalStudentIdList);

// saves the new student mappings to the studentMapping-repo
List<StudentMappingEntity> entityList = new ArrayList<>();
for (final StudentMappingInput studentMappingInput : studentMappingInputs) {
entityList.add(assignmentMapper.studentMappingInputToEntity(studentMappingInput));
}
studentMappingRepository.saveAll(entityList);

// retries parsing all unfinishedGradingEntities
List<UnfinishedGradingEntity> unfinishedGradingEntityList = unfinishedGradingRepository.findAll();
for (final UnfinishedGradingEntity unfinishedGradingEntity : unfinishedGradingEntityList) {
JSONObject jsonObject = new JSONObject(unfinishedGradingEntity.getGradingJson());
AssignmentEntity assignmentEntity = assignmentService.requireAssignmentExists(unfinishedGradingEntity.getId().getAssignmentId());

try {
GradingEntity gradingEntity = parseIntoGradingEntity(jsonObject, assignmentEntity);
gradingRepository.save(gradingEntity);
logGradingImported(gradingEntity);
unfinishedGradingRepository.deleteById(unfinishedGradingEntity.getId());
} catch (ManualMappingRequiredException e) {
// should not happen, because all mappings should be done at this instance
}

}

// returns all the newly mapped external student ids
return externalStudentIdList;
}

public List<ManualMappingInstance> getManualMappingInstances(final UUID courseId, final LoggedInUser currentUser) {
try {
validateUserHasAccessToCourse(currentUser, LoggedInUser.UserRoleInCourse.ADMINISTRATOR, courseId);
} catch (final NoAccessToCourseException ex) {
return null;
}

List<ManualMappingInstanceEntity> entityList = manualMappingInstanceRepository.findAll();

return entityList.stream().map(assignmentMapper::manualMappingInstanceEntityToDto).toList();
}

/**
* Gets external assignment information from TMS. <br>
* This is needed for mapping MEITREX-Assignments to TMS-Assignments.
Expand Down
58 changes: 57 additions & 1 deletion src/main/resources/graphql/service/grading.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,60 @@ type StudentMapping {
Student Id in external system like TMS
"""
externalStudentId: String!
}
}

input StudentMappingInput {
"""
Student Id in Meitrex
"""
meitrexStudentId: UUID!

"""
Student Id in external system like TMS
"""
externalStudentId: String!
}

"""
An object to represent a student where the backend could not automatically map the external student to a meitrex user.
"""
type ManualMappingInstance {
"""
Student Id in external system like TMS
"""
externalStudentId: String!

"""
JSON Object containing all available information on the external student.
"""
externalStudentInfo: String!
}


"""
An Unfinished Grading is created, when importing and parsing gradings from external systems like TMS goes wrong
because the student id has to be mapped manually.
After an admin mapped ids manually, these unfinished gradings will be tried again.
"""
type UnfinishedGrading {
"""
Student Id in external system like TMS
"""
externalStudentId: String!

"""
Assignment/HandIn id in MEITREX
"""
assignmentId: UUID!

"""
JSON representation of the grading
"""
gradingJson: String!

"""
The number of times importing and parsing was tried. Might be useful for detecting and manually deleting broken gradings.
"""
numberOfTries: Int!
}

10 changes: 9 additions & 1 deletion src/main/resources/graphql/service/mutation.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type Mutation {

"""
Modify an assignment.
πŸ”’ The user must be an admin the course the assignment is in to perform this action.
πŸ”’ The user must be an admin in the course the assignment is in to perform this action.
"""
mutateAssignment(assessmentId: UUID!): AssignmentMutation!

Expand All @@ -21,6 +21,14 @@ type Mutation {
πŸ”’ The user must be a tutor in the course the assignment is in to perform this action.
""" # TODO keep description up to date throughout development (especially the required roles)
logAssignmentCompleted(input: LogAssignmentCompletedInput!): AssignmentCompletedFeedback!

"""
Saves mappings of meitrex users to external students.
Response to ManualMappingInstances.
Returns list of all deleted ManualMappingInstance ids.
πŸ”’ The user must be an admin in the course to perform this action.
"""
saveStudentMappings(courseId: UUID!, studentMappingInputs: [StudentMappingInput!]!): [String]!
}

type AssignmentMutation {
Expand Down
10 changes: 8 additions & 2 deletions src/main/resources/graphql/service/query.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@ type Query {
"""
Gets all the available external exercises.
CourseId is the id of the course the user is currently working is.
πŸ”’ The user must be an admin in the course. Otherwise null is returned for
an assignment if the user has no access to it.
πŸ”’ The user must be an admin in the course. Otherwise null is returned.
"""
getExternalAssignments(courseId: UUID!): [ExternalAssignment!]!


"""
Gets all manual student mappings, i.e. all students where the backend could not map to a meitrex user.
πŸ”’ The user must be an admin in the course. Otherwise null is returned.
"""
getManualMappingInstances(courseId: UUID!): [ManualMappingInstance]!
}

0 comments on commit 32c3d1a

Please sign in to comment.