Skip to content

Commit

Permalink
[#12048] Migrate Course Action classes (#12092)
Browse files Browse the repository at this point in the history
* [#12048] Set up github action workflows

* [#12048] v9: Skeleton implementation (#12056)

* [#12048] Add isMigrated flag to course (#12063)

* [#12048] Add is migrated flag to datastore account (#12070)

* Temporarily disable liquibase migrations

* [#12048] Create Notification Entity for PostgreSQL migration (#12061)

* [#12048] Create notification DB layer for v9 migration (#12075)

* [#12048] Add UsageStatistics entity and db (#12076)

* Migrate GetCourseAction.java

* Migrate DeleteCourseAction.java and relevant logic functions

* Migrate BinCourseAction.java and its related logic functions

* Update checkSpecificAccessControl functions in BinCourseAction and DeleteCourseAction classes

* Migrate RestoreCourseAction and its related logic functions

* Migrate UpdateCourseAction with its related logic functions

* [#12048] Add Account Entity (#12087)

* [#12048] Create SQL logic for CreateNotificationAction and add relevant tests for v9 migration (#12077)

* [#12048] Create Student, Instructor and User Entities for PostgreSQL Migration (#12071)

* [#12048] V9: Cleanup and refactor (#12090)

* Edit GetCourseAction and refactor out the old datastore code

* [#12048] Remove redundant InstructorRole Enum (#12091)

* Fix compilation error

* Update check for database to fetch from

* Add unit tests for CoursesDb

* [#12048] Update GetUsageStatisticsAction to include SQL entities (#12084)

* Add CoursesLogicTest class

* Disable failing tests

* Fix compilation error

* Fix Checkstyle errors

* Merge branch

* Change flow for updating courses.

* Update updateCourse JavaDoc comment.

* Update CreateCourseAction and related methods

* Update GetCourseAction.

* Update UpdateCourseAction

* Update BinCourseAction and RestoreCourseAction

* Update DeleteCourseAction

* Migrate GetCourseSectionNamesAction and related methods.

* Add Unit tests for Logic layer of Course.

* Fix Checkstyle errors

* Add unit test for GetCourseAction's execute function

* Add verify for CoursesDb unit tests and use assertNull and assertNotNull

* Move fetching of course to logic layer.

* Fix Checkstyle errors.

* Move canCreateCourse logic to logic layer.

* Change *CourseAction classes to use isCourseMigrated

* Fix CoursesLogic's initLogicDependencies method call

* Add unit tests for GetCourseAction.

* Remove commented out method.

* Add minimal unit tests for BinCourseAction, DeleteCourseAction and RestoreCourseAction.

* Add minimal unit tests for GetCourseSectionAction and UpdateCourseAction.

* Remove unused EntityType parameter.

* Add minimal unit tests for CreateCourseAction.

* Fix Checkstyle errors.

* Ignore all old datastore test cases for *CourseAction classes.

* Fix 'text' type to 'test'.

* Change binCourseToRecycleBin to return the binned course.

* Update moveCourseToRecycleBin test.

* Update test name.

---------

Co-authored-by: Samuel Fang <[email protected]>
Co-authored-by: dao ngoc hieu <[email protected]>
Co-authored-by: Samuel Fang <[email protected]>
Co-authored-by: wuqirui <[email protected]>
Co-authored-by: Dominic Lim <[email protected]>
  • Loading branch information
6 people committed Mar 20, 2023
1 parent 53524a2 commit 839c642
Show file tree
Hide file tree
Showing 29 changed files with 1,647 additions and 71 deletions.
58 changes: 58 additions & 0 deletions src/main/java/teammates/sqllogic/api/Logic.java
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,50 @@ public Course createCourse(Course course) throws InvalidParametersException, Ent
return coursesLogic.createCourse(course);
}

/**
* Deletes a course by course id.
* @param courseId of course.
*/
public void deleteCourseCascade(String courseId) {
coursesLogic.deleteCourseCascade(courseId);
}

/**
* Moves a course to Recycle Bin by its given corresponding ID.
* @return the deletion timestamp assigned to the course.
*/
public Course moveCourseToRecycleBin(String courseId) throws EntityDoesNotExistException {
return coursesLogic.moveCourseToRecycleBin(courseId);
}

/**
* Restores a course and all data related to the course from Recycle Bin by
* its given corresponding ID.
*/
public void restoreCourseFromRecycleBin(String courseId) throws EntityDoesNotExistException {
coursesLogic.restoreCourseFromRecycleBin(courseId);
}

/**
* Updates a course.
*
* @return updated course
* @throws InvalidParametersException if attributes to update are not valid
* @throws EntityDoesNotExistException if the course cannot be found
*/
public Course updateCourse(String courseId, String name, String timezone)
throws InvalidParametersException, EntityDoesNotExistException {
return coursesLogic.updateCourse(courseId, name, timezone);
}

/**
* Gets a list of section names for the given {@code courseId}.
*/
public List<String> getSectionNamesForCourse(String courseId)
throws EntityDoesNotExistException {
return coursesLogic.getSectionNamesForCourse(courseId);
}

/**
* Get section by {@code courseId} and {@code teamName}.
*/
Expand Down Expand Up @@ -389,6 +433,13 @@ public Instructor getInstructorByGoogleId(String courseId, String googleId) {
return usersLogic.getInstructorByGoogleId(courseId, googleId);
}

/**
* Gets list of instructors by {@code googleId}.
*/
public List<Instructor> getInstructorsForGoogleId(String googleId) {
return usersLogic.getInstructorsForGoogleId(googleId);
}

/**
* Gets instructors by associated {@code courseId}.
*/
Expand All @@ -404,6 +455,13 @@ public Instructor createInstructor(Instructor instructor)
return usersLogic.createInstructor(instructor);
}

/**
* Checks if an instructor with {@code googleId} can create a course with {@code institute}.
*/
public boolean canInstructorCreateCourse(String googleId, String institute) {
return usersLogic.canInstructorCreateCourse(googleId, institute);
}

/**
* Gets student associated with {@code id}.
*
Expand Down
99 changes: 99 additions & 0 deletions src/main/java/teammates/sqllogic/core/CoursesLogic.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package teammates.sqllogic.core;

import static teammates.common.util.Const.ERROR_UPDATE_NON_EXISTENT;

import java.time.Instant;
import java.util.List;
import java.util.stream.Collectors;

import teammates.common.exception.EntityAlreadyExistsException;
import teammates.common.exception.EntityDoesNotExistException;
import teammates.common.exception.InvalidParametersException;
import teammates.storage.sqlapi.CoursesDb;
import teammates.storage.sqlentity.Course;
Expand Down Expand Up @@ -53,6 +60,81 @@ public Course getCourse(String courseId) {
return coursesDb.getCourse(courseId);
}

/**
* Deletes a course and cascade its students, instructors, sessions, responses, deadline extensions and comments.
* Fails silently if no such course.
*/
public void deleteCourseCascade(String courseId) {
Course course = coursesDb.getCourse(courseId);
if (course == null) {
return;
}

// TODO: Migrate after other Logic classes have been migrated.
// AttributesDeletionQuery query = AttributesDeletionQuery.builder()
// .withCourseId(courseId)
// .build();
// frcLogic.deleteFeedbackResponseComments(query);
// frLogic.deleteFeedbackResponses(query);
// fqLogic.deleteFeedbackQuestions(query);
// feedbackSessionsLogic.deleteFeedbackSessions(query);
// studentsLogic.deleteStudents(query);
// instructorsLogic.deleteInstructors(query);
// deadlineExtensionsLogic.deleteDeadlineExtensions(query);

coursesDb.deleteCourse(course);
}

/**
* Moves a course to Recycle Bin by its given corresponding ID.
* @return the time when the course is moved to the recycle bin.
*/
public Course moveCourseToRecycleBin(String courseId) throws EntityDoesNotExistException {
Course course = coursesDb.getCourse(courseId);
if (course == null) {
throw new EntityDoesNotExistException("Trying to move a non-existent course to recycling bin.");
}

Instant now = Instant.now();
course.setDeletedAt(now);
return course;
}

/**
* Restores a course from Recycle Bin by its given corresponding ID.
*/
public void restoreCourseFromRecycleBin(String courseId) throws EntityDoesNotExistException {
Course course = coursesDb.getCourse(courseId);
if (course == null) {
throw new EntityDoesNotExistException("Trying to restore a non-existent course from recycling bin.");
}

course.setDeletedAt(null);
}

/**
* Updates a course.
*
* @return updated course
* @throws InvalidParametersException if attributes to update are not valid
* @throws EntityDoesNotExistException if the course cannot be found
*/
public Course updateCourse(String courseId, String name, String timezone)
throws InvalidParametersException, EntityDoesNotExistException {
Course course = getCourse(courseId);
if (course == null) {
throw new EntityDoesNotExistException(ERROR_UPDATE_NON_EXISTENT + Course.class);
}
course.setName(name);
course.setTimeZone(timezone);

if (!course.isValid()) {
throw new InvalidParametersException(course.getInvalidityInfo());
}

return course;
}

/**
* Creates a section.
*/
Expand All @@ -70,6 +152,23 @@ public Section getSectionByCourseIdAndTeam(String courseId, String teamName) {
return coursesDb.getSectionByCourseIdAndTeam(courseId, teamName);
}

/**
* Gets a list of section names for the given {@code courseId}.
*/
public List<String> getSectionNamesForCourse(String courseId) throws EntityDoesNotExistException {
assert courseId != null;
Course course = getCourse(courseId);

if (course == null) {
throw new EntityDoesNotExistException("Trying to get section names for a non-existent course.");
}

return course.getSections()
.stream()
.map(section -> section.getName())
.collect(Collectors.toList());
}

/**
* Creates a team.
*/
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/teammates/sqllogic/core/UsersLogic.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ public List<Instructor> getInstructorsForCourse(String courseId) {
return instructorReturnList;
}

/**
* Gets all instructors associated with a googleId.
*/
public List<Instructor> getInstructorsForGoogleId(String googleId) {
assert googleId != null;
return usersDb.getInstructorsForGoogleId(googleId);
}

/**
* Returns true if the user associated with the googleId is an instructor in any course in the system.
*/
Expand Down Expand Up @@ -263,4 +271,20 @@ public void resetStudentGoogleId(String email, String courseId, String googleId)
public static <T extends User> void sortByName(List<T> users) {
users.sort(Comparator.comparing(user -> user.getName().toLowerCase()));
}

/**
* Checks if an instructor with {@code googleId} can create a course with {@code institute}
* (ie. has an existing course(s) with the same {@code institute}).
*/
public boolean canInstructorCreateCourse(String googleId, String institute) {
assert googleId != null;
assert institute != null;

List<Instructor> existingInstructors = getInstructorsForGoogleId(googleId);
return existingInstructors
.stream()
.filter(Instructor::hasCoownerPrivileges)
.map(instructor -> instructor.getCourse())
.anyMatch(course -> institute.equals(course.getInstitute()));
}
}
15 changes: 15 additions & 0 deletions src/main/java/teammates/storage/sqlapi/UsersDb.java
Original file line number Diff line number Diff line change
Expand Up @@ -305,4 +305,19 @@ public List<Student> getAllStudentsForEmail(String email) {
return HibernateUtil.createQuery(cr).getResultList();
}

/**
* Gets all instructors associated with a googleId.
*/
public List<Instructor> getInstructorsForGoogleId(String googleId) {
assert googleId != null;

CriteriaBuilder cb = HibernateUtil.getCriteriaBuilder();
CriteriaQuery<Instructor> cr = cb.createQuery(Instructor.class);
Root<Instructor> instructorRoot = cr.from(Instructor.class);
Join<Instructor, Account> accountsJoin = instructorRoot.join("account");

cr.select(instructorRoot).where(cb.equal(accountsJoin.get("googleId"), googleId));

return HibernateUtil.createQuery(cr).getResultList();
}
}
12 changes: 12 additions & 0 deletions src/main/java/teammates/storage/sqlentity/Course.java
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ public List<FeedbackSession> getFeedbackSessions() {
return feedbackSessions;
}

public void setFeedbackSessions(List<FeedbackSession> feedbackSessions) {
this.feedbackSessions = feedbackSessions;
}

public List<Section> getSections() {
return sections;
}

public void setSections(List<Section> sections) {
this.sections = sections;
}

public Instant getUpdatedAt() {
return updatedAt;
}
Expand Down
28 changes: 21 additions & 7 deletions src/main/java/teammates/ui/webapi/BinCourseAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import teammates.common.datatransfer.attributes.CourseAttributes;
import teammates.common.exception.EntityDoesNotExistException;
import teammates.common.util.Const;
import teammates.storage.sqlentity.Course;
import teammates.ui.output.CourseData;

/**
* Move a course to the recycle bin.
*/
class BinCourseAction extends Action {
public class BinCourseAction extends Action {

@Override
AuthType getMinAuthLevel() {
Expand All @@ -22,18 +23,31 @@ void checkSpecificAccessControl() throws UnauthorizedAccessException {
}

String idOfCourseToBin = getNonNullRequestParamValue(Const.ParamsNames.COURSE_ID);
gateKeeper.verifyAccessible(logic.getInstructorForGoogleId(idOfCourseToBin, userInfo.id),
logic.getCourse(idOfCourseToBin), Const.InstructorPermissions.CAN_MODIFY_COURSE);

if (!isCourseMigrated(idOfCourseToBin)) {
CourseAttributes courseAttributes = logic.getCourse(idOfCourseToBin);
gateKeeper.verifyAccessible(logic.getInstructorForGoogleId(idOfCourseToBin, userInfo.id),
courseAttributes, Const.InstructorPermissions.CAN_MODIFY_COURSE);
return;
}

Course course = sqlLogic.getCourse(idOfCourseToBin);
gateKeeper.verifyAccessible(sqlLogic.getInstructorByGoogleId(idOfCourseToBin, userInfo.id),
course, Const.InstructorPermissions.CAN_MODIFY_COURSE);
}

@Override
public JsonResult execute() {
String idOfCourseToBin = getNonNullRequestParamValue(Const.ParamsNames.COURSE_ID);
try {
CourseAttributes courseAttributes = logic.getCourse(idOfCourseToBin);
courseAttributes.setDeletedAt(logic.moveCourseToRecycleBin(idOfCourseToBin));

return new JsonResult(new CourseData(courseAttributes));
if (!isCourseMigrated(idOfCourseToBin)) {
CourseAttributes courseAttributes = logic.getCourse(idOfCourseToBin);
courseAttributes.setDeletedAt(logic.moveCourseToRecycleBin(idOfCourseToBin));
return new JsonResult(new CourseData(courseAttributes));
}

Course binnedCourse = sqlLogic.moveCourseToRecycleBin(idOfCourseToBin);
return new JsonResult(new CourseData(binnedCourse));
} catch (EntityDoesNotExistException e) {
throw new EntityNotFoundException(e);
}
Expand Down
26 changes: 8 additions & 18 deletions src/main/java/teammates/ui/webapi/CreateCourseAction.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
package teammates.ui.webapi;

import java.util.List;
import java.util.Objects;

import teammates.common.datatransfer.attributes.InstructorAttributes;
import teammates.common.exception.EntityAlreadyExistsException;
import teammates.common.exception.InvalidParametersException;
import teammates.common.util.Const;
import teammates.common.util.FieldValidator;
import teammates.common.util.HibernateUtil;
import teammates.storage.sqlentity.Course;
import teammates.storage.sqlentity.Instructor;
import teammates.ui.output.CourseData;
import teammates.ui.request.CourseCreateRequest;
import teammates.ui.request.InvalidHttpRequestBodyException;

/**
* Create a new course for an instructor.
*/
class CreateCourseAction extends Action {
public class CreateCourseAction extends Action {

@Override
AuthType getMinAuthLevel() {
Expand All @@ -32,13 +29,8 @@ void checkSpecificAccessControl() throws UnauthorizedAccessException {

String institute = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_INSTITUTION);

List<InstructorAttributes> existingInstructors = logic.getInstructorsForGoogleId(userInfo.getId());
boolean canCreateCourse = existingInstructors
.stream()
.filter(InstructorAttributes::hasCoownerPrivileges)
.map(instructor -> logic.getCourse(instructor.getCourseId()))
.filter(Objects::nonNull)
.anyMatch(course -> institute.equals(course.getInstitute()));
boolean canCreateCourse = sqlLogic.canInstructorCreateCourse(userInfo.getId(), institute);

if (!canCreateCourse) {
throw new UnauthorizedAccessException("You are not allowed to create a course under this institute. "
+ "If you wish to do so, please request for an account under the institute.", true);
Expand All @@ -64,13 +56,11 @@ public JsonResult execute() throws InvalidHttpRequestBodyException, InvalidOpera
Course course = new Course(newCourseId, newCourseName, newCourseTimeZone, institute);

try {
sqlLogic.createCourse(course); // TODO: Create instructor as well
course = sqlLogic.createCourse(course);

// TODO: Migrate once instructor entity is ready.
// InstructorAttributes instructorCreatedForCourse = logic.getInstructorForGoogleId(newCourseId,
// userInfo.getId());
// taskQueuer.scheduleInstructorForSearchIndexing(instructorCreatedForCourse.getCourseId(),
// instructorCreatedForCourse.getEmail());
Instructor instructorCreatedForCourse = sqlLogic.getInstructorByGoogleId(newCourseId, userInfo.getId());
taskQueuer.scheduleInstructorForSearchIndexing(instructorCreatedForCourse.getCourseId(),
instructorCreatedForCourse.getEmail());
} catch (EntityAlreadyExistsException e) {
throw new InvalidOperationException("The course ID " + course.getId()
+ " has been used by another course, possibly by some other user."
Expand Down
Loading

0 comments on commit 839c642

Please sign in to comment.