Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#12048] Migrate GetSessionResultsAction #12719

Merged
merged 52 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
e1955b6
Migrate SessionResultsData
xenosf Feb 12, 2024
8f0f835
Add default entities
xenosf Feb 12, 2024
d40f2fb
Add helper methods to assist migrated logic
xenosf Feb 12, 2024
da63d4a
Migrate buildCompleteGiverRecipientMap
xenosf Feb 12, 2024
cc8ff7a
Migrate checkSpecificAccessControl
xenosf Feb 1, 2024
a046670
Add default team instance for instructor
xenosf Feb 4, 2024
9b4945b
Migrate session results data logic
xenosf Feb 4, 2024
fb5a808
Use default team entity for instructor instead of const
xenosf Feb 12, 2024
cd64a4c
Migrate non-db logic
xenosf Feb 14, 2024
a78bf65
Merge branch 'v9-migration' into v9-migration-GetSessionResultsAction
xenosf Feb 14, 2024
97ae4b5
Refactor Datastore and SQL action logic out to separate methods
xenosf Feb 15, 2024
51f738f
Fix checkstyle errors
xenosf Feb 15, 2024
6573955
Migrate DB logic
xenosf Feb 16, 2024
13191fe
Merge branch 'v9-migration' into v9-migration-GetSessionResultsAction
xenosf Feb 19, 2024
80d651e
Fix checkstyle errors
xenosf Feb 19, 2024
7e960f7
Move default instructor team entity to const
xenosf Feb 19, 2024
901307a
Add test for SqlSessionResultsBundle
xenosf Feb 19, 2024
29900bd
Fix SQL results bundle test
xenosf Feb 20, 2024
ab20259
Add IT for GetSessionResultsAction
xenosf Feb 20, 2024
06c6b61
Fix action logic
xenosf Feb 20, 2024
0ad3b56
Fix checkstyle errors
xenosf Feb 20, 2024
a23545d
Remove unused method parameters
xenosf Feb 20, 2024
1aa00a8
Merge branch 'v9-migration' into v9-migration-GetSessionResultsAction
xenosf Feb 20, 2024
56a9d93
Fix persistence issues in test cases
xenosf Feb 20, 2024
a5de57d
Remove question getter for comment
xenosf Feb 23, 2024
6e62b70
Rename boolean methods to start with verb
xenosf Feb 23, 2024
fa89559
Reword comment to clarify question ID
xenosf Feb 23, 2024
273032b
Refactor getting question UUID from param value
xenosf Feb 23, 2024
8627c3f
Remove unneeded getters
xenosf Feb 23, 2024
7f7b81b
Remove entities from Const
xenosf Feb 23, 2024
b6d4b0f
Revert changes to SqlCourseRoster
xenosf Feb 23, 2024
bd67d3c
Create and use missing response class
xenosf Feb 23, 2024
092a08b
Merge branch 'v9-migration' into v9-migration-GetSessionResultsAction
xenosf Feb 23, 2024
0668931
Refactor no response text to const
xenosf Feb 24, 2024
82eeeb8
Merge branch 'v9-migration' into v9-migration-GetSessionResultsAction
xenosf Feb 24, 2024
f850787
Merge branch 'v9-migration' into v9-migration-GetSessionResultsAction
xenosf Feb 24, 2024
a012ad2
Migrate preview-related functionality
xenosf Feb 24, 2024
4401890
Merge branch 'v9-migration' into v9-migration-GetSessionResultsAction
xenosf Feb 24, 2024
2504ead
Migrate preview functionality for question output
xenosf Feb 24, 2024
3811154
Fix recipient section filter
xenosf Feb 26, 2024
08ca697
Merge branch 'master' into v9-migration-GetSessionResultsAction
xenosf Feb 26, 2024
f8c18b8
Update test cases to handle question preview
xenosf Feb 26, 2024
4a75f0e
Merge duplicate methods
xenosf Feb 26, 2024
27ca58d
Fix checkstyle errors
xenosf Feb 26, 2024
79fae78
Add missing questions with non-visible preview responses
xenosf Feb 26, 2024
809b006
Merge branch 'master' into v9-migration-GetSessionResultsAction
xenosf Feb 26, 2024
5d8bf95
Remove outdated test
xenosf Feb 26, 2024
e10e4ba
Edit for style and readability
xenosf Feb 26, 2024
343bd0e
Fix missing join
xenosf Feb 26, 2024
36eb927
Fix section filtering logic
xenosf Feb 26, 2024
f762d0e
Fix checkstyle errors
xenosf Feb 26, 2024
d34caa1
Merge branch 'master' into v9-migration-GetSessionResultsAction
xenosf Feb 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
361 changes: 361 additions & 0 deletions src/it/java/teammates/it/ui/webapi/GetSessionResultsActionIT.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package teammates.common.datatransfer;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import teammates.common.util.Const;
import teammates.common.util.StringHelper;
import teammates.storage.sqlentity.FeedbackQuestion;
import teammates.storage.sqlentity.FeedbackResponse;
import teammates.storage.sqlentity.FeedbackResponseComment;

/**
* Represents detailed results for a feedback session.
*/
public class SqlSessionResultsBundle {

private final List<FeedbackQuestion> questions;
private final Set<FeedbackQuestion> questionsNotVisibleForPreviewSet;
private final Set<FeedbackQuestion> questionsWithCommentNotVisibleForPreviewSet;
private final Map<FeedbackQuestion, List<FeedbackResponse>> questionResponseMap;
private final Map<FeedbackQuestion, List<FeedbackResponse>> questionMissingResponseMap;
private final Map<FeedbackResponse, List<FeedbackResponseComment>> responseCommentsMap;
private final Map<FeedbackResponse, Boolean> responseGiverVisibilityTable;
private final Map<FeedbackResponse, Boolean> responseRecipientVisibilityTable;
private final Map<Long, Boolean> commentGiverVisibilityTable;
private final SqlCourseRoster roster;

public SqlSessionResultsBundle(List<FeedbackQuestion> questions,
Set<FeedbackQuestion> questionsNotVisibleForPreviewSet,
Set<FeedbackQuestion> questionsWithCommentNotVisibleForPreviewSet,
List<FeedbackResponse> responses,
List<FeedbackResponse> missingResponses,
Map<FeedbackResponse, Boolean> responseGiverVisibilityTable,
Map<FeedbackResponse, Boolean> responseRecipientVisibilityTable,
Map<FeedbackResponse, List<FeedbackResponseComment>> responseCommentsMap,
Map<Long, Boolean> commentGiverVisibilityTable,
SqlCourseRoster roster) {

this.questions = questions;
this.questionsNotVisibleForPreviewSet = questionsNotVisibleForPreviewSet;
this.questionsWithCommentNotVisibleForPreviewSet = questionsWithCommentNotVisibleForPreviewSet;
this.responseCommentsMap = responseCommentsMap;
this.responseGiverVisibilityTable = responseGiverVisibilityTable;
this.responseRecipientVisibilityTable = responseRecipientVisibilityTable;
this.commentGiverVisibilityTable = commentGiverVisibilityTable;
this.roster = roster;
this.questionResponseMap = buildQuestionToResponseMap(responses);
this.questionMissingResponseMap = buildQuestionToResponseMap(missingResponses);
}

private Map<FeedbackQuestion, List<FeedbackResponse>> buildQuestionToResponseMap(
List<FeedbackResponse> responses) {
// build question to response map
Map<FeedbackQuestion, List<FeedbackResponse>> questionToResponseMap = new LinkedHashMap<>();
for (FeedbackQuestion question : questions) {
questionToResponseMap.put(question, new ArrayList<>());
}
for (FeedbackResponse response : responses) {
FeedbackQuestion question = response.getFeedbackQuestion();
List<FeedbackResponse> responsesForQuestion = questionToResponseMap.get(question);
responsesForQuestion.add(response);
}
return questionToResponseMap;
}

/**
* Returns true if the giver of a response is visible to the current user.
* Returns false otherwise.
*/
public boolean isResponseGiverVisible(FeedbackResponse response) {
return isResponseParticipantVisible(true, response);
}

/**
* Returns true if the recipient of a response is visible to the current user.
* Returns false otherwise.
*/
public boolean isResponseRecipientVisible(FeedbackResponse response) {
return isResponseParticipantVisible(false, response);
}

/**
* Checks if the giver/recipient for a response is visible/hidden from the current user.
*/
private boolean isResponseParticipantVisible(boolean isGiver, FeedbackResponse response) {
FeedbackQuestion question = response.getFeedbackQuestion();
FeedbackParticipantType participantType;

boolean isVisible;
if (isGiver) {
isVisible = responseGiverVisibilityTable.get(response);
participantType = question.getGiverType();
} else {
isVisible = responseRecipientVisibilityTable.get(response);
participantType = question.getRecipientType();
}
boolean isTypeNone = participantType == FeedbackParticipantType.NONE;

return isVisible || isTypeNone;
}

/**
* Returns true if the giver of a comment is visible to the current user.
* Returns false otherwise.
*/
public boolean isCommentGiverVisible(FeedbackResponseComment comment) {
return commentGiverVisibilityTable.get(comment.getId());
}

/**
* Gets the anonymous name for a given name.
*
* <p>The anonymous name will be deterministic based on {@code name}.
*/
public static String getAnonName(FeedbackParticipantType type, String name) {
String hashedEncryptedName = getHashOfName(getEncryptedName(name));
String participantType = type.toSingularFormString();
return String.format(
Const.DISPLAYED_NAME_FOR_ANONYMOUS_PARTICIPANT + " %s %s", participantType, hashedEncryptedName);
}

public Map<FeedbackQuestion, List<FeedbackResponse>> getQuestionResponseMap() {
return questionResponseMap;
}

public Map<FeedbackQuestion, List<FeedbackResponse>> getQuestionMissingResponseMap() {
return questionMissingResponseMap;
}

private static String getEncryptedName(String name) {
return StringHelper.encrypt(name);
}

private static String getHashOfName(String name) {
return Long.toString(Math.abs((long) name.hashCode()));
}

public List<FeedbackQuestion> getQuestions() {
return questions;
}

public Map<FeedbackResponse, List<FeedbackResponseComment>> getResponseCommentsMap() {
return responseCommentsMap;
}

public SqlCourseRoster getRoster() {
return roster;
}

public Map<FeedbackResponse, Boolean> getResponseGiverVisibilityTable() {
return responseGiverVisibilityTable;
}

public Map<FeedbackResponse, Boolean> getResponseRecipientVisibilityTable() {
return responseRecipientVisibilityTable;
}

public Map<Long, Boolean> getCommentGiverVisibilityTable() {
return commentGiverVisibilityTable;
}

public Set<FeedbackQuestion> getQuestionsNotVisibleForPreviewSet() {
return questionsNotVisibleForPreviewSet;
}

public Set<FeedbackQuestion> getQuestionsWithCommentNotVisibleForPreviewSet() {
return questionsWithCommentNotVisibleForPreviewSet;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import teammates.common.datatransfer.FeedbackParticipantType;
import teammates.common.datatransfer.SessionResultsBundle;
import teammates.common.datatransfer.SqlSessionResultsBundle;
import teammates.common.datatransfer.attributes.FeedbackQuestionAttributes;
import teammates.common.util.JsonUtils;
import teammates.storage.sqlentity.FeedbackQuestion;
Expand Down Expand Up @@ -41,6 +42,19 @@ public String getQuestionResultStatisticsJson(
return "";
}

/**
* Get question result statistics as JSON string.
*/
@SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract")
public String getQuestionResultStatisticsJson(
FeedbackQuestion question, String studentEmail, SqlSessionResultsBundle bundle) {
// Statistics are calculated in the front-end as it is dependent on the responses being filtered.
// The only exception is contribution question, where there is only one statistics for the entire question.
// It is also necessary to calculate contribution question statistics here
// to be displayed in student result page as students are not supposed to be able to see the exact responses.
return "";
}

/**
* Checks whether the changes to the question details require deletion of corresponding responses.
*/
Expand Down Expand Up @@ -106,6 +120,17 @@ public boolean shouldGenerateMissingResponses(FeedbackQuestionAttributes questio
&& question.getRecipientType() != FeedbackParticipantType.TEAMS_EXCLUDING_SELF;
}

/**
* Checks whether missing responses should be generated.
*/
public boolean shouldGenerateMissingResponses(FeedbackQuestion question) {
// generate combinations against all students/teams are meaningless
return question.getRecipientType() != FeedbackParticipantType.STUDENTS
&& question.getRecipientType() != FeedbackParticipantType.STUDENTS_EXCLUDING_SELF
&& question.getRecipientType() != FeedbackParticipantType.TEAMS
&& question.getRecipientType() != FeedbackParticipantType.TEAMS_EXCLUDING_SELF;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/teammates/common/util/Const.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public final class Const {
public static final String ERROR_CREATE_ENTITY_ALREADY_EXISTS = "Trying to create an entity that exists: %s";
public static final String ERROR_UPDATE_NON_EXISTENT = "Trying to update non-existent Entity: ";

public static final String MISSING_RESPONSE_TEXT = "No Response";

// These constants are used as variable values to mean that the variable is in a 'special' state.

public static final int INT_UNINITIALIZED = -9999;
Expand Down
35 changes: 35 additions & 0 deletions src/main/java/teammates/sqllogic/api/Logic.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import javax.annotation.Nullable;

import teammates.common.datatransfer.FeedbackQuestionRecipient;
import teammates.common.datatransfer.FeedbackResultFetchType;
import teammates.common.datatransfer.NotificationStyle;
import teammates.common.datatransfer.NotificationTargetUser;
import teammates.common.datatransfer.SqlDataBundle;
import teammates.common.datatransfer.SqlSessionResultsBundle;
import teammates.common.exception.EnrollException;
import teammates.common.exception.EntityAlreadyExistsException;
import teammates.common.exception.EntityDoesNotExistException;
Expand Down Expand Up @@ -1227,6 +1229,39 @@ public List<FeedbackQuestion> getFeedbackQuestionsForInstructors(
return feedbackQuestionsLogic.getFeedbackQuestionsForInstructors(feedbackSession, instructorEmail);
}

/**
* Gets the session result for a feedback session.
*
* @see FeedbackResponsesLogic#getSessionResultsForCourse(
* FeedbackSession, String, String, String, Section, FeedbackResultFetchType)
*/
public SqlSessionResultsBundle getSessionResultsForCourse(
FeedbackSession feedbackSession, String courseId, String userEmail,
@Nullable UUID questionId, @Nullable String sectionName, @Nullable FeedbackResultFetchType fetchType) {
assert feedbackSession != null;
assert courseId != null;
assert userEmail != null;

return feedbackResponsesLogic.getSessionResultsForCourse(
feedbackSession, courseId, userEmail, questionId, sectionName, fetchType);
}

/**
* Gets the session result for a feedback session for the given user.
*
* @see FeedbackResponsesLogic#getSessionResultsForUser(FeedbackSession, String, String, boolean, String)
*/
public SqlSessionResultsBundle getSessionResultsForUser(
FeedbackSession feedbackSession, String courseId, String userEmail, boolean isInstructor,
@Nullable UUID questionId, boolean isPreviewResults) {
assert feedbackSession != null;
assert courseId != null;
assert userEmail != null;

return feedbackResponsesLogic.getSessionResultsForUser(
feedbackSession, courseId, userEmail, isInstructor, questionId, isPreviewResults);
}

/**
* Persists the given data bundle to the database.
*/
Expand Down
101 changes: 101 additions & 0 deletions src/main/java/teammates/sqllogic/core/FeedbackQuestionsLogic.java
Original file line number Diff line number Diff line change
Expand Up @@ -647,4 +647,105 @@ public List<FeedbackQuestion> getFeedbackQuestionForCourseWithType(
.collect(Collectors.toList());
}

/**
* Builds a complete giver to recipient map for a {@code relatedQuestion}.
*
* @param relatedQuestion The question to be considered
* @param courseRoster the roster in the course
* @return a map from giver to recipient for the question.
*/
public Map<String, Set<String>> buildCompleteGiverRecipientMap(
FeedbackQuestion relatedQuestion, SqlCourseRoster courseRoster) {
Map<String, Set<String>> completeGiverRecipientMap = new HashMap<>();

List<String> possibleGiverEmails = getPossibleGivers(relatedQuestion, courseRoster);
for (String possibleGiverEmail : possibleGiverEmails) {
switch (relatedQuestion.getGiverType()) {
case STUDENTS:
Student studentGiver = courseRoster.getStudentForEmail(possibleGiverEmail);
completeGiverRecipientMap
.computeIfAbsent(possibleGiverEmail, key -> new HashSet<>())
.addAll(getRecipientsOfQuestion(
relatedQuestion, null, studentGiver, courseRoster).keySet());
break;
case TEAMS:
Student oneTeamMember =
courseRoster.getTeamToMembersTable().get(possibleGiverEmail).iterator().next();
completeGiverRecipientMap
.computeIfAbsent(possibleGiverEmail, key -> new HashSet<>())
.addAll(getRecipientsOfQuestion(
relatedQuestion, null, oneTeamMember, courseRoster).keySet());
break;
case INSTRUCTORS:
case SELF:
Instructor instructorGiver = courseRoster.getInstructorForEmail(possibleGiverEmail);

// only happens when a session creator quits their course
if (instructorGiver == null) {
instructorGiver = new Instructor(
relatedQuestion.getCourse(),
USER_NAME_FOR_SELF,
possibleGiverEmail,
false,
USER_NAME_FOR_SELF,
null,
null
);
}

completeGiverRecipientMap
.computeIfAbsent(possibleGiverEmail, key -> new HashSet<>())
.addAll(getRecipientsOfQuestion(
relatedQuestion, instructorGiver, null, courseRoster).keySet());
break;
default:
log.severe("Invalid giver type specified");
break;
}
}

return completeGiverRecipientMap;
}

/**
* Gets possible giver identifiers for a feedback question.
*
* @param fq the feedback question
* @param courseRoster roster of all students and instructors
* @return a list of giver identifier
*/
private List<String> getPossibleGivers(
FeedbackQuestion fq, SqlCourseRoster courseRoster) {
FeedbackParticipantType giverType = fq.getGiverType();
List<String> possibleGivers = new ArrayList<>();

switch (giverType) {
case STUDENTS:
possibleGivers = courseRoster.getStudents()
.stream()
.map(Student::getEmail)
.collect(Collectors.toList());
break;
case INSTRUCTORS:
possibleGivers = courseRoster.getInstructors()
.stream()
.map(Instructor::getEmail)
.collect(Collectors.toList());
break;
case TEAMS:
possibleGivers = new ArrayList<>(courseRoster.getTeamToMembersTable().keySet());
break;
case SELF:
FeedbackSession feedbackSession =
feedbackSessionsLogic.getFeedbackSession(fq.getFeedbackSessionName(), fq.getCourseId());
possibleGivers = Collections.singletonList(feedbackSession.getCreatorEmail());
break;
default:
log.severe("Invalid giver type specified");
break;
}

return possibleGivers;
}

}
Loading
Loading