diff --git a/.github/workflows/axe.yml b/.github/workflows/axe.yml index 771dd8a3edcd..8507df2a98a9 100644 --- a/.github/workflows/axe.yml +++ b/.github/workflows/axe.yml @@ -16,17 +16,17 @@ jobs: matrix: browser: [firefox] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 18 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: - distribution: 'temurin' - java-version: '11' + distribution: temurin + java-version: 11 - name: Cache Gradle packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/caches diff --git a/.github/workflows/comment-goodfirstissue.yml b/.github/workflows/comment-goodfirstissue.yml index 97c001be75fe..b5c788944131 100644 --- a/.github/workflows/comment-goodfirstissue.yml +++ b/.github/workflows/comment-goodfirstissue.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Add comment - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/component.yml b/.github/workflows/component.yml index 11ecfb7357f0..77a246454d50 100644 --- a/.github/workflows/component.yml +++ b/.github/workflows/component.yml @@ -19,19 +19,19 @@ jobs: os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: '0' - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 18 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: - distribution: 'temurin' - java-version: '11' + distribution: temurin + java-version: 11 - name: Cache Gradle packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/caches @@ -40,7 +40,7 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - name: Cache eslint - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ./.eslintcache @@ -48,7 +48,7 @@ jobs: restore-keys: | ${{ runner.os }}-eslint- - name: Cache stylelint - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ./.stylelintcache @@ -68,17 +68,17 @@ jobs: os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 18 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: - distribution: 'temurin' - java-version: '11' + distribution: temurin + java-version: 11 - name: Cache Gradle packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/caches diff --git a/.github/workflows/dev-docs.yml b/.github/workflows/dev-docs.yml index 07d2416241a5..61e9954ea54d 100644 --- a/.github/workflows/dev-docs.yml +++ b/.github/workflows/dev-docs.yml @@ -17,14 +17,14 @@ jobs: shell: bash working-directory: docs steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 18 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: - java-version: '11' - distribution: 'temurin' + java-version: 11 + distribution: temurin - name: Install Graphviz run: sudo apt install graphviz - run: npm ci diff --git a/.github/workflows/e2e-sql.yml b/.github/workflows/e2e-sql.yml index 0a67cbac52d1..c1d70b8b1f57 100644 --- a/.github/workflows/e2e-sql.yml +++ b/.github/workflows/e2e-sql.yml @@ -20,17 +20,17 @@ jobs: browser: [firefox] tests: [stable, unstable] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 18 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: - distribution: 'temurin' - java-version: '11' + distribution: temurin + java-version: 11 - name: Cache Gradle packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/caches @@ -53,4 +53,4 @@ jobs: ./gradlew serverRun & ./wait-for-server.sh - name: Start Tests - run: xvfb-run --server-args="-screen 0 1024x768x24" ./gradlew -P${{ matrix.tests }} e2eTestsSql \ No newline at end of file + run: xvfb-run --server-args="-screen 0 1024x768x24" ./gradlew -P${{ matrix.tests }} e2eTestsSql diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 52ab76019ed2..565253ba2f46 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -20,17 +20,17 @@ jobs: browser: [firefox] tests: [stable, unstable] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 18 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: - distribution: 'temurin' - java-version: '11' + distribution: temurin + java-version: 11 - name: Cache Gradle packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/caches @@ -54,4 +54,3 @@ jobs: ./wait-for-server.sh - name: Start Tests run: xvfb-run --server-args="-screen 0 1024x768x24" ./gradlew -P${{ matrix.tests }} e2eTests - \ No newline at end of file diff --git a/.github/workflows/jdk17.yml b/.github/workflows/jdk17.yml index 29ab59bb5619..7b50447b7aae 100644 --- a/.github/workflows/jdk17.yml +++ b/.github/workflows/jdk17.yml @@ -9,9 +9,9 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 @@ -20,9 +20,9 @@ jobs: component-testing: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 diff --git a/.github/workflows/jdk21.yml b/.github/workflows/jdk21.yml new file mode 100644 index 000000000000..cb551fc44fa5 --- /dev/null +++ b/.github/workflows/jdk21.yml @@ -0,0 +1,34 @@ +name: Component Tests (JDK 21) + +on: + push: + branches: + - master + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + - name: Run Backend Linting + run: ./gradlew lint --continue + component-testing: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + - name: Update Property File + run: mv src/test/resources/test.ci-ubuntu-latest.properties src/test/resources/test.properties + - name: Run Solr search service + run: docker-compose run -d -p 8983:8983 solr + - name: Run Backend Tests + run: ./gradlew createConfigs componentTests diff --git a/.github/workflows/lnp.yml b/.github/workflows/lnp.yml index 2ff33dda9206..595ad697efc1 100644 --- a/.github/workflows/lnp.yml +++ b/.github/workflows/lnp.yml @@ -11,14 +11,14 @@ jobs: LnP-testing: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: - distribution: 'temurin' - java-version: '11' + distribution: temurin + java-version: 11 - name: Cache Gradle packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/caches diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 03f11d9b6d38..7885aa57cd8c 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -13,7 +13,7 @@ jobs: permissions: pull-requests: write steps: - - uses: actions/github-script@v6 + - uses: actions/github-script@v7 with: script: | const pr = await github.rest.pulls.get({ diff --git a/build.gradle b/build.gradle index 90abb909473c..c31de8ad72de 100644 --- a/build.gradle +++ b/build.gradle @@ -8,10 +8,10 @@ apply plugin: "jacoco" apply plugin: "cz.habarta.typescript-generator" apply plugin: "org.liquibase.gradle" -def checkstyleVersion = "10.3.2" -def pmdVersion = "6.48.0" -def spotbugsVersion = "4.7.1" -def jacocoVersion = "0.8.8" +def checkstyleVersion = "10.15.0" +def pmdVersion = "7.0.0" +def spotbugsVersion = "4.8.4" +def jacocoVersion = "0.8.12" buildscript { repositories { @@ -21,13 +21,13 @@ buildscript { } } dependencies { - classpath "com.google.cloud.tools:appengine-gradle-plugin:2.4.4" - classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.9" - classpath("cz.habarta.typescript-generator:typescript-generator-gradle-plugin:2.36.1070") { + classpath "com.google.cloud.tools:appengine-gradle-plugin:2.8.0" + classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.12" + classpath("cz.habarta.typescript-generator:typescript-generator-gradle-plugin:3.2.1263") { exclude group: "org.gradle" } - classpath "com.google.guava:guava:31.1-jre" - classpath "org.liquibase:liquibase-gradle-plugin:2.1.1" + classpath "com.google.guava:guava:33.1.0-jre" + classpath "org.liquibase:liquibase-gradle-plugin:2.2.1" } } @@ -37,12 +37,19 @@ configurations { liquibaseRuntime.extendsFrom testImplementation } +// See https://github.com/checkstyle/checkstyle/issues/14211#issuecomment-1884129948 +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability("com.google.collections:google-collections") { + select("com.google.guava:guava:0") + } +} + repositories { mavenCentral() } def objectify = "com.googlecode.objectify:objectify:6.0.7" -def testng = "org.testng:testng:7.6.1" +def testng = "org.testng:testng:7.10.1" dependencies { staticAnalysis("com.puppycrawl.tools:checkstyle:${checkstyleVersion}") @@ -52,56 +59,56 @@ dependencies { annotationProcessor(objectify) - implementation("com.google.auth:google-auth-library-oauth2-http:1.8.1") - implementation(platform("com.google.cloud:google-cloud-bom:0.176.0")) + implementation("com.google.auth:google-auth-library-oauth2-http:1.23.0") + implementation(platform("com.google.cloud:google-cloud-bom:0.218.0")) implementation("com.google.cloud:google-cloud-datastore") implementation("com.google.cloud:google-cloud-tasks") implementation("com.google.cloud:google-cloud-logging") - implementation("com.google.code.gson:gson:2.9.0") - implementation("com.google.firebase:firebase-admin:9.1.1") - implementation("com.google.guava:guava:31.1-jre") + implementation("com.google.code.gson:gson:2.10.1") + implementation("com.google.firebase:firebase-admin:9.2.0") + implementation("com.google.guava:guava:33.1.0-jre") implementation(objectify) - implementation("com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20220608.1") + implementation("com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1") implementation("com.helger:ph-commons:9.5.5") // necessary to add SpotBugs suppression - implementation("com.mailjet:mailjet-client:5.2.0") - implementation("com.sendgrid:sendgrid-java:4.9.3") + implementation("com.mailjet:mailjet-client:5.2.5") + implementation("com.sendgrid:sendgrid-java:4.10.2") implementation("com.sun.jersey:jersey-client:1.19.4") implementation("com.sun.jersey:jersey-core:1.19.4") implementation("com.sun.jersey.contribs:jersey-multipart:1.19.4") - implementation("org.apache.solr:solr-solrj:8.11.1") - implementation(platform("org.eclipse.jetty:jetty-bom:10.0.13")) + implementation("org.apache.solr:solr-solrj:8.11.3") + implementation(platform("org.eclipse.jetty:jetty-bom:11.0.20")) implementation("org.eclipse.jetty:jetty-slf4j-impl") implementation("org.eclipse.jetty:jetty-server") implementation("org.eclipse.jetty:jetty-webapp") implementation("org.eclipse.jetty:jetty-annotations") - implementation("org.jsoup:jsoup:1.15.2") - implementation("org.hibernate.orm:hibernate-core:6.1.6.Final") - implementation("org.postgresql:postgresql:42.7.2") - implementation("org.hibernate:hibernate-hikaricp:6.1.6.Final") + implementation("org.jsoup:jsoup:1.17.2") + implementation("org.hibernate.orm:hibernate-core:6.4.4.Final") + implementation("org.postgresql:postgresql:42.7.3") + implementation("org.hibernate:hibernate-hikaricp:6.4.4.Final") testAnnotationProcessor(testng) testImplementation("com.tngtech.archunit:archunit:0.11.0") - testImplementation("junit:junit:4.13.2") - testImplementation("org.seleniumhq.selenium:selenium-java:4.3.0") - testImplementation("com.deque.html.axe-core:selenium:4.6.0") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2") + testImplementation("org.seleniumhq.selenium:selenium-java:4.19.1") + testImplementation("com.deque.html.axe-core:selenium:4.9.0") testImplementation(testng) - testImplementation("org.testcontainers:postgresql:1.17.6") - testImplementation("org.liquibase:liquibase-core:4.19.0") - testImplementation("org.mockito:mockito-core:5.1.1") + testImplementation("org.testcontainers:postgresql:1.19.7") + testImplementation("org.liquibase:liquibase-core:4.27.0") + testImplementation("org.mockito:mockito-core:5.11.0") // For supporting authorization code flow locally - testImplementation("com.google.oauth-client:google-oauth-client-jetty:1.34.1") + testImplementation("com.google.oauth-client:google-oauth-client-jetty:1.35.0") // For using Gmail API - testImplementation("com.google.apis:google-api-services-gmail:v1-rev20220404-1.32.1") + testImplementation("com.google.apis:google-api-services-gmail:v1-rev20231218-2.0.0") // For using JMeter APIs - testImplementation("org.apache.jmeter:ApacheJMeter_core:5.5") { + testImplementation("org.apache.jmeter:ApacheJMeter_core:5.6.2") { exclude group: "org.apache.jmeter", module: "bom" } - testImplementation("org.apache.jmeter:ApacheJMeter_http:5.5") { + testImplementation("org.apache.jmeter:ApacheJMeter_http:5.6.2") { exclude group: "org.apache.jmeter", module: "bom" } - liquibaseRuntime("info.picocli:picocli:4.7.1") + liquibaseRuntime("info.picocli:picocli:4.7.5") liquibaseRuntime(sourceSets.main.output) } @@ -136,6 +143,10 @@ sourceSets { } } +if (!project.hasProperty("runList")) { + project.ext.set("runList", "main") +} + liquibase { activities { main { @@ -145,13 +156,29 @@ liquibase { username project.properties['liquibaseUsername'] password project.properties['liquibasePassword'] } + snapshot { + url project.properties['liquibaseDbUrl'] + username project.properties['liquibaseUsername'] + password project.properties['liquibasePassword'] + snapshotFormat "json" + outputFile "liquibase-snapshot.json" + } + diffMain { + searchPath "${projectDir}" + changeLogFile "src/main/resources/db/changelog/db.changelog-new.xml" + referenceUrl project.properties['liquibaseDbUrl'] + referenceUsername project.properties['liquibaseUsername'] + referencePassword project.properties['liquibasePassword'] + url "offline:postgresql?snapshot=liquibase-snapshot.json" + } } + runList = project.ext.runList } tasks.withType(cz.habarta.typescript.generator.gradle.GenerateTask) { jsonLibrary = "jackson2" optionalAnnotations = [ - "javax.annotation.Nullable" + "jakarta.annotation.Nullable" ] outputFileType = "implementationFile" outputKind = "module" @@ -392,16 +419,16 @@ pmd { } spotbugs { - reportLevel = "low" + reportLevel = com.github.spotbugs.snom.Confidence.valueOf("LOW") toolVersion = spotbugsVersion includeFilter = file("$rootProject.projectDir/static-analysis/teammates-spotbugs.xml") } tasks.withType(com.github.spotbugs.snom.SpotBugsTask) { reports { - xml.enabled = false - html.enabled = false - text.enabled = true + xml.required = false + html.required = false + text.required = true } } @@ -612,6 +639,8 @@ task e2eTests { afterTest afterTestClosure if (isFirstTry) { afterSuite checkTestNgFailureClosure + } else { + mustRunAfter "e2eTestTry${id - 1}" } onlyIf { isFirstTry || file("${buildDir}/reports/${outputFileName}${id - 1}/testng-failed.xml").exists() @@ -649,6 +678,8 @@ task e2eTestsSql { afterTest afterTestClosure if (isFirstTry) { afterSuite checkTestNgFailureClosure + } else { + mustRunAfter "e2eSqlTestTry${id - 1}" } onlyIf { isFirstTry || file("${buildDir}/reports/${outputFileName}${id - 1}/testng-failed.xml").exists() @@ -681,6 +712,8 @@ task axeTests { afterTest afterTestClosure if (isFirstTry) { afterSuite checkTestNgFailureClosure + } else { + mustRunAfter "axeTestTry${id - 1}" } onlyIf { isFirstTry || file("${buildDir}/reports/axe-test-try-${id - 1}/testng-failed.xml").exists() diff --git a/docs/_markbind/layouts/default.md b/docs/_markbind/layouts/default.md index f92791366a06..e57ee418a494 100644 --- a/docs/_markbind/layouts/default.md +++ b/docs/_markbind/layouts/default.md @@ -27,6 +27,7 @@ * [Captcha]({{ baseUrl }}/captcha.html) * [Documentation]({{ baseUrl }}/documentation.html) * [Emails]({{ baseUrl }}/emails.html) + * [Unit Testing]({{ baseUrl }}/unit-testing.html) * [End-to-End Testing]({{ baseUrl }}/e2e-testing.html) * [Performance Testing]({{ baseUrl }}/performance-testing.html) * [Accessibility Testing]({{ baseUrl }}/axe-testing.html) diff --git a/docs/development.md b/docs/development.md index 703a1ce57b08..aec8c3696caa 100644 --- a/docs/development.md +++ b/docs/development.md @@ -291,7 +291,9 @@ There are two big categories of testing in TEAMMATES: - **Component tests**: white-box unit and integration tests, i.e. they test the application components with full knowledge of the components' internal workings. This is configured in `src/test/resources/testng-component.xml` (back-end) and `src/web/jest.config.js` (front-end). - **E2E (end-to-end) tests**: black-box tests, i.e. they test the application as a whole without knowing any internal working. This is configured in `src/e2e/resources/testng-e2e.xml`. To learn more about E2E tests, refer to this [document](e2e-testing.md). -#### Running the tests +
+ +#### Running tests ##### Frontend tests @@ -335,6 +337,8 @@ You can generate the coverage data with `jacocoReport` task after running tests, The report can be found in the `build/reports/jacoco/jacocoReport/` directory. +
+ ## Deploying to a staging server > `Staging server` is the server instance you set up on Google App Engine for hosting the app for testing purposes. diff --git a/docs/schema-migration.md b/docs/schema-migration.md new file mode 100644 index 000000000000..a681a7bc4ff8 --- /dev/null +++ b/docs/schema-migration.md @@ -0,0 +1,35 @@ + + title: "Schema Migration" + + +# SQL Schema Migration + +Teammates uses _[Liquibase]_(https://docs.liquibase.com/start/home.html), a database schema change management solution that enables developers to revise and release database changes to production. The maintainers in charge of releases (Release Leader) will be in charge of generating a _Liquibase_ changelog prior to each release to keep the production databases schema in sync with the code. Therefore this section is just for documentation purposes for contributors. + +## Liquibase in Teammates +_Liquibase_ is made available using the [gradle plugin](https://github.com/liquibase/liquibase-gradle-plugin), providing _liquibase_ functions as tasks. Try `gradle tasks | grep "liquibase"` to see all the tasks available. In teammates, change logs (more in the next section) are written in _XML_. + +### Liquibase connection +Amend the `liquibaseDbUrl`, `liquibaseUsername` and `liquibasePassword` in `gradle.properties` to allow the _Liquibase_ plugin to connect your database. + +## Change logs, change sets and change types +A _change log_ is a file that contains a series of _change sets_ (analagous to a transaction) which applies _change types_ (actions). You can refer to this page on liquibase on the types of [change types](https://docs.liquibase.com/change-types/home.html) that can be used. + +## Gradle Activities for Liquibase +Activities in Gradle are a way of specifying different variables provided by gradle to the Liquibase plugin. The argument `runList` provided by `-pRunList=` e.g `./gradlew liquibaseSnapshot -PrunList=snapshot` is used to specify which activity to be used for the Liquibase command. In this case the `liquibaseSnapshot` command is run using the `snapshot` activity. + +Here is a brief description of the activities defined for Liquibase +1. Main: The default activity used by Liquibase commands and is used for running changelogs against a database. This is used by default if a `runList` is not defined +2. Snapshot: Used to specify output format and name for snapshots i.e JSON +3. diffMain: Specify the reference and the target database to generate changelog that contains operations to update reference database to the state of the target database. i.e the reference is the JSON file generated by the snapshot command, this can be replaced with a live database which is used as reference. + +## Generating/ Updating liquibase change logs +1. Ensure `diff-main` activity in `build.gradle` is pointing to the latest release changelog `src/main/resources/db/changelog/db.changelog-.xml` +2. Delete the `postgres-data` folder to clear any old database schemas +3. Run `git checkout ` and +4. Run the server using `./gradlew serverRun` to generate tables found on branch +5. Generate snapshot of database by running `./gradlew liquibaseSnapshot -PrunList=snapshot`, the snapshot will be output to `liquibase-snapshot.json` +6. Checkout your branch and repeat steps 2 and 4 to generate the tables found on your branch +7. Run `./gradlew liquibaseDiffChangeLog -PrunList=diffMain` to generate changeLog to resolve database schema differences +8. Manually add a changeSet to tag the database using `release_number`. The tag should be named ` + title: "Unit Testing" + + +# Unit Testing + +## What is Unit Testing? + +Unit testing is a testing methodology where the objective is to test components in isolation. + +- It aims to ensure all components of the application work as expected, assuming its dependencies are working. +- This is done in TEAMMATES by using mocks to simulate a component's dependencies. + +Frontend Unit tests in TEAMMATES are located in `.spec.ts` files, while Backend Unit tests in TEAMMATES can be found in the package `teammates.test`. + + +## Writing Unit Tests + +### General guidelines + +#### Include only relevant details in tests +When writing unit tests, reduce the amount of noise in the code to make it easier for future developers to follow. + +The code below has a lot of noise in creation of the `studentModel`: + +```javascript +it('displayInviteButton: should display "Send Invite" button when a student has not joined the course', () => { + component.studentModels = [ + { + student: { + name: 'tester', + teamName: 'Team 1', + email: 'tester@tester.com', + joinState: JoinState.NOT_JOINED, + sectionName: 'Tutorial Group 1', + courseId: 'text-exa.demo', + }, + isAllowedToViewStudentInSection: true, + isAllowedToModifyStudent: true, + }, + ]; + + expect(sendInviteButton).toBeTruthy(); +}); +``` + +However, what is important is only the student joinState. We should thus reduce the noise by including only the relevant details: + +```javascript +it('displayInviteButton: should display "Send Invite" button when a student has not joined the course', () => { + component.studentModels = [ + studentModelBuilder + .joinState(JoinState.NOT_JOINED) + .build() + ]; + + expect(sendInviteButton).toBeTruthy(); +}); +``` + +Including only the relevant details in tests makes it easier for future developers to read and understand the purpose of the test. + +#### Favor readability over uniqueness +Since tests don't have tests, it should be easy for developers to manually inspect them for correctness, even at the expense of greater code duplication. + +Take the following test for example: + +```java +@BeforeMethod +public void setUp() { + users = new User[]{new User("alice"), new User("bob")}; +} + +@Test +public void test_register_canRegisterMultipleUsers() { + registerAllUsers(); + for (User user : users) { + assertTrue(forum.hasRegisteredUser(user)); + } +} + +private void registerAllUsers() { + for (User user : users) { + forum.register(user); + } +} +``` + +While the code reduces duplication, it is not as straightforward for a developer to follow. + +A more readable way to write this test would be: +```java +@Test +public void test_register_canRegisterMultipleUsers() { + User user1 = new User("alice"); + User user2 = new User("bob"); + + forum.register(user1); + forum.register(user2); + + assertTrue(forum.hasRegisteredUser(user1)); + assertTrue(forum.hasRegisteredUser(user2)); +} +``` + +By choosing readability over uniqueness in writing unit tests, there is code duplication, but the test flow is easier for a reader to follow. + + +#### Inline mocks in test code + +Inlining mock return values in the unit test itself improves readability: + +```javascript +it('getStudentCourseJoinStatus: should return true if student has joined the course' , () => { + jest.spyOn(courseService, 'getJoinCourseStatus') + .mockReturnValue(of({ hasJoined: true })); + + expect(student.getJoinCourseStatus).toBeTruthy(); +}); +``` + +By injecting the values in the test right before they are used, developers are able to more easily trace the code and understand the test. + +### Frontend + +#### Naming +Unit tests for a function should follow the format: + +`": should ... when/if ..."` + +Example: + +```javascript + it('hasSection: should return false when there are no sections in the course') +``` + +#### Creating test data +To aid with [including only relevant details in tests](#include-only-relevant-details-in-tests), use the builder in `src/web/test-helpers/generic-builder.ts` + +Usage: +```javascript +const instructorModelBuilder = createBuilder({ + email: 'instructor@gmail.com', + name: 'Instructor', + hasSubmittedSession: false, + isSelected: false, +}); + +it('isAllInstructorsSelected: should return false if at least one instructor !isSelected', () => { +component.instructorListInfoTableRowModels = [ + instructorModelBuilder.isSelected(true).build(), + instructorModelBuilder.isSelected(false).build(), + instructorModelBuilder.isSelected(true).build(), +]; + +expect(component.isAllInstructorsSelected).toBeFalsy(); +}); + +``` + +#### Testing event emission +In Angular, child components emit events. To test for event emissions, we've provided a utility function in `src/test-helpers/test-event-emitter` + +Usage: +```javascript +@Output() +deleteCommentEvent: EventEmitter = new EventEmitter(); + +triggerDeleteCommentEvent(index: number): void { + this.deleteCommentEvent.emit(index); +} + +it('triggerDeleteCommentEvent: should emit the correct index to deleteCommentEvent', () => { + let emittedIndex: number | undefined; + testEventEmission(component.deleteCommentEvent, (index) => { emittedIndex = index; }); + + component.triggerDeleteCommentEvent(5); + expect(emittedIndex).toBe(5); +}); +``` + +### Backend + +#### Naming +Unit test names should follow the format: `test__` + +Examples: +```java +public void testGetComment_commentDoesNotExist_returnsNull() +public void testCreateComment_commentDoesNotExist_success() +public void testCreateComment_commentAlreadyExists_throwsEntityAlreadyExistsException() +``` + +#### Creating test data +To aid with [including only relevant details in tests](#include-only-relevant-details-in-tests), use the `getTypicalX` functions in `BaseTestCase`, where X represents an entity. + +Example: +```java +Account account = getTypicalAccount(); +account.setEmail("newemail@teammates.com"); + +Student student = getTypicalStudent(); +student.setName("New Student Name"); +``` + + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090..e6441136f3d4 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2ec77e51a9c9..e7646dead063 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index a69d9cb6c206..1aa94a426907 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index f127cfd49d40..25da30dbdeee 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -42,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/solr/solr.sh b/solr/solr.sh index 401022a87d7f..f29234bccd65 100755 --- a/solr/solr.sh +++ b/solr/solr.sh @@ -17,3 +17,5 @@ bin/solr create -c accountrequests -s 2 -rf 2 bin/solr config -c accountrequests -p 8983 -action set-user-property -property update.autoCreateFields -value false curl -X POST -H 'Content-type: application/json' --data-binary '{"add-field": {"name": "email", "type": "string"}}' localhost:8983/solr/accountrequests/schema curl -X POST -H 'Content-type: application/json' --data-binary '{"add-field": {"name": "institute", "type": "string"}}' localhost:8983/solr/accountrequests/schema +curl -X POST -H 'Content-type: application/json' --data-binary '{"add-field": {"name": "comments", "type": "string"}}' localhost:8983/solr/accountrequests/schema +curl -X POST -H 'Content-type: application/json' --data-binary '{"add-field": {"name": "status", "type": "string"}}' localhost:8983/solr/accountrequests/schema diff --git a/src/client/java/teammates/client/connector/DatastoreClient.java b/src/client/java/teammates/client/connector/DatastoreClient.java index 6b8f4510fb36..a699379adcd3 100644 --- a/src/client/java/teammates/client/connector/DatastoreClient.java +++ b/src/client/java/teammates/client/connector/DatastoreClient.java @@ -1,5 +1,6 @@ package teammates.client.connector; +import com.google.cloud.NoCredentials; import com.google.cloud.datastore.DatastoreOptions; import com.googlecode.objectify.Objectify; import com.googlecode.objectify.ObjectifyFactory; @@ -43,7 +44,7 @@ protected void doOperationRemotely() { ObjectifyService.init(new ObjectifyFactory(builder.build().getService())); OfyHelper.registerEntityClasses(); - try (Closeable objectifySession = ObjectifyService.begin()) { + try (Closeable ignored = ObjectifyService.begin()) { LogicStarter.initializeDependencies(); doOperation(); } diff --git a/src/client/java/teammates/client/scripts/sql/DataMigrationForAccountRequestSql.java b/src/client/java/teammates/client/scripts/sql/DataMigrationForAccountRequestSql.java index bef8085455b1..21abcf9e4620 100644 --- a/src/client/java/teammates/client/scripts/sql/DataMigrationForAccountRequestSql.java +++ b/src/client/java/teammates/client/scripts/sql/DataMigrationForAccountRequestSql.java @@ -3,6 +3,7 @@ // CHECKSTYLE.OFF:ImportOrder import com.googlecode.objectify.cmd.Query; +import teammates.common.datatransfer.AccountRequestStatus; import jakarta.persistence.criteria.CriteriaDelete; import teammates.common.util.HibernateUtil; @@ -57,7 +58,9 @@ protected void migrateEntity(teammates.storage.entity.AccountRequest oldEntity) AccountRequest newEntity = new AccountRequest( oldEntity.getEmail(), oldEntity.getName(), - oldEntity.getInstitute()); + oldEntity.getInstitute(), + AccountRequestStatus.APPROVED, + null); // set registration key to the old value if exists if (oldEntity.getRegistrationKey() != null) { diff --git a/src/client/java/teammates/client/scripts/sql/IndexCourseFields.java b/src/client/java/teammates/client/scripts/sql/IndexCourseFields.java new file mode 100644 index 000000000000..283bc8e2da95 --- /dev/null +++ b/src/client/java/teammates/client/scripts/sql/IndexCourseFields.java @@ -0,0 +1,37 @@ +package teammates.client.scripts.sql; + +import com.googlecode.objectify.cmd.Query; + +import teammates.client.scripts.DataMigrationEntitiesBaseScript; +import teammates.storage.entity.Course; + +/** + * Index the newly-indexable fields of courses. + */ +public class IndexCourseFields extends DataMigrationEntitiesBaseScript { + + public static void main(String[] args) { + new IndexCourseFields().doOperationRemotely(); + } + + @Override + protected Query getFilterQuery() { + return ofy().load().type(Course.class); + } + + @Override + protected boolean isPreview() { + return true; + } + + @Override + protected boolean isMigrationNeeded(Course course) { + return true; + } + + @Override + protected void migrateEntity(Course course) { + // Save without any update; this will build the previously non-existing indexes + saveEntityDeferred(course); + } +} diff --git a/src/client/java/teammates/client/scripts/sql/SeedDb.java b/src/client/java/teammates/client/scripts/sql/SeedDb.java index a80731e42d93..f4c72357a946 100644 --- a/src/client/java/teammates/client/scripts/sql/SeedDb.java +++ b/src/client/java/teammates/client/scripts/sql/SeedDb.java @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; diff --git a/src/client/java/teammates/client/scripts/sql/VerifyAccountAttributes.java b/src/client/java/teammates/client/scripts/sql/VerifyAccountAttributes.java index b53f684640ce..c978d56be6a0 100644 --- a/src/client/java/teammates/client/scripts/sql/VerifyAccountAttributes.java +++ b/src/client/java/teammates/client/scripts/sql/VerifyAccountAttributes.java @@ -92,7 +92,7 @@ public boolean equals(teammates.storage.sqlentity.Account sqlEntity, Account dat // Map datastoreReadNotifications = datastoreEntity.getReadNotifications(); // List sqlReadNotifications = sqlEntity.getReadNotifications(); - // List datastoreEndTimes = new ArrayList(datastoreReadNotifications.values()); + // List datastoreEndTimes = new ArrayList<>(datastoreReadNotifications.values()); // Collections.sort(datastoreEndTimes); // List sqlEndTimes = new ArrayList<>(); diff --git a/src/client/java/teammates/client/scripts/sql/VerifyDataMigrationConnection.java b/src/client/java/teammates/client/scripts/sql/VerifyDataMigrationConnection.java index 909c8ac649ef..c26147b40161 100644 --- a/src/client/java/teammates/client/scripts/sql/VerifyDataMigrationConnection.java +++ b/src/client/java/teammates/client/scripts/sql/VerifyDataMigrationConnection.java @@ -7,6 +7,7 @@ import teammates.client.connector.DatastoreClient; import teammates.client.util.ClientProperties; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.util.HibernateUtil; import teammates.storage.entity.UsageStatistics; import teammates.storage.sqlentity.Notification; @@ -43,7 +44,9 @@ protected void verifySqlConnection() { teammates.storage.sqlentity.AccountRequest newEntity = new teammates.storage.sqlentity.AccountRequest( "dummy-teammates-account-request-email@gmail.com", "dummy-teammates-account-request", - "dummy-teammates-institute"); + "dummy-teammates-institute", + AccountRequestStatus.PENDING, + "dummy-comments"); HibernateUtil.beginTransaction(); HibernateUtil.persist(newEntity); HibernateUtil.commitTransaction(); diff --git a/src/client/java/teammates/client/scripts/statistics/FileStore.java b/src/client/java/teammates/client/scripts/statistics/FileStore.java index 66dd6018123a..7a45056f8b0c 100644 --- a/src/client/java/teammates/client/scripts/statistics/FileStore.java +++ b/src/client/java/teammates/client/scripts/statistics/FileStore.java @@ -135,7 +135,7 @@ private static Gson getSerializer() { /** * An adapter for Gson to serialize {@link Instant} type. */ - private static class InstantAdapter implements JsonSerializer, JsonDeserializer { + private static final class InstantAdapter implements JsonSerializer, JsonDeserializer { @Override public JsonElement serialize(Instant instant, Type type, JsonSerializationContext context) { diff --git a/src/client/java/teammates/client/scripts/testdataconversion/DataStoreToSqlConverter.java b/src/client/java/teammates/client/scripts/testdataconversion/DataStoreToSqlConverter.java index 69acaad79753..b814918478f9 100644 --- a/src/client/java/teammates/client/scripts/testdataconversion/DataStoreToSqlConverter.java +++ b/src/client/java/teammates/client/scripts/testdataconversion/DataStoreToSqlConverter.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.UUID; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.datatransfer.InstructorPermissionRole; import teammates.common.datatransfer.attributes.AccountAttributes; import teammates.common.datatransfer.attributes.AccountRequestAttributes; @@ -38,32 +39,44 @@ * Helper class to convert entities from its noSQL to SQL format. */ public class DataStoreToSqlConverter { - private String uuidPrefix = "00000000-0000-4000-8000-"; - private int initialAccountNumber = 1; - private int initialAccountRequestNumber = 101; - private int initialSectionNumber = 201; - private int initialTeamNumber = 301; - private int initialDeadlineExtensionNumber = 401; - private int initialInstructorNumber = 501; - private int initialStudentNumber = 601; - private int intitialFeedbackSessionNumber = 701; - private int initialFeedbackQuestionNumber = 801; - private int intialFeedbackResponseNumber = 901; - private int initialNotificationNumber = 1101; - private int initialReadNotificationNumber = 1201; - - private UuidGenerator accountUuidGenerator = new UuidGenerator(initialAccountNumber, uuidPrefix); - private UuidGenerator accounRequestUuidGenerator = new UuidGenerator(initialAccountRequestNumber, uuidPrefix); - private UuidGenerator sectionUuidGenerator = new UuidGenerator(initialSectionNumber, uuidPrefix); - private UuidGenerator teamUuidGenerator = new UuidGenerator(initialTeamNumber, uuidPrefix); - private UuidGenerator deadlineExtensionUuidGenerator = new UuidGenerator(initialDeadlineExtensionNumber, uuidPrefix); - private UuidGenerator instructorUuidGenerator = new UuidGenerator(initialInstructorNumber, uuidPrefix); - private UuidGenerator studentUuidGenerator = new UuidGenerator(initialStudentNumber, uuidPrefix); - private UuidGenerator feedbackSessionUuidGenerator = new UuidGenerator(intitialFeedbackSessionNumber, uuidPrefix); - private UuidGenerator feedbackQuestionUuidGenerator = new UuidGenerator(initialFeedbackQuestionNumber, uuidPrefix); - private UuidGenerator feedbackResponseUuidGenerator = new UuidGenerator(intialFeedbackResponseNumber, uuidPrefix); - private UuidGenerator notificationUuidGenerator = new UuidGenerator(initialNotificationNumber, uuidPrefix); - private UuidGenerator readNotificationUuidGenerator = new UuidGenerator(initialReadNotificationNumber, uuidPrefix); + private static final String UUID_PREFIX = "00000000-0000-4000-8000-"; + private static final int INITIAL_ACCOUNT_NUMBER = 1; + private static final int INITIAL_ACCOUNT_REQUEST_NUMBER = 101; + private static final int INITIAL_SECTION_NUMBER = 201; + private static final int INITIAL_TEAM_NUMBER = 301; + private static final int INITIAL_DEADLINE_EXTENSION_NUMBER = 401; + private static final int INITIAL_INSTRUCTOR_NUMBER = 501; + private static final int INITIAL_STUDENT_NUMBER = 601; + private static final int INITIAL_FEEDBACK_SESSION_NUMBER = 701; + private static final int INITIAL_FEEDBACK_QUESTION_NUMBER = 801; + private static final int INITIAL_FEEDBACK_RESPONSE_NUMBER = 901; + private static final int INITIAL_NOTIFICATION_NUMBER = 1101; + private static final int INITIAL_READ_NOTIFICATION_NUMBER = 1201; + + private static final UuidGenerator ACCOUNT_UUID_GENERATOR = + new UuidGenerator(INITIAL_ACCOUNT_NUMBER, UUID_PREFIX); + private static final UuidGenerator ACCOUN_REQUEST_UUID_GENERATOR = + new UuidGenerator(INITIAL_ACCOUNT_REQUEST_NUMBER, UUID_PREFIX); + private static final UuidGenerator SECTION_UUID_GENERATOR = + new UuidGenerator(INITIAL_SECTION_NUMBER, UUID_PREFIX); + private static final UuidGenerator TEAM_UUID_GENERATOR = + new UuidGenerator(INITIAL_TEAM_NUMBER, UUID_PREFIX); + private static final UuidGenerator DEADLINE_EXTENSION_UUID_GENERATOR = + new UuidGenerator(INITIAL_DEADLINE_EXTENSION_NUMBER, UUID_PREFIX); + private static final UuidGenerator INSTRUCTOR_UUID_GENERATOR = + new UuidGenerator(INITIAL_INSTRUCTOR_NUMBER, UUID_PREFIX); + private static final UuidGenerator STUDENT_UUID_GENERATOR = + new UuidGenerator(INITIAL_STUDENT_NUMBER, UUID_PREFIX); + private static final UuidGenerator FEEDBACK_SESSION_UUID_GENERATOR = + new UuidGenerator(INITIAL_FEEDBACK_SESSION_NUMBER, UUID_PREFIX); + private static final UuidGenerator FEEDBACK_QUESTION_UUID_GENERATOR = + new UuidGenerator(INITIAL_FEEDBACK_QUESTION_NUMBER, UUID_PREFIX); + private static final UuidGenerator FEEDBACK_RESPONSE_UUID_GENERATOR = + new UuidGenerator(INITIAL_FEEDBACK_RESPONSE_NUMBER, UUID_PREFIX); + private static final UuidGenerator NOTIFICATION_UUID_GENERATOR = + new UuidGenerator(INITIAL_NOTIFICATION_NUMBER, UUID_PREFIX); + private static final UuidGenerator READ_NOTIFICATION_UUID_GENERATOR = + new UuidGenerator(INITIAL_READ_NOTIFICATION_NUMBER, UUID_PREFIX); private long initialFeedbackResponseCommentId; @@ -98,11 +111,11 @@ private String generateSectionKey(String courseId, String sectionName) { return String.format("%s-%s", courseId, sectionName); } - private String generatefeedbackSessionKey(FeedbackSessionAttributes feedbackSession) { + private String generateFeedbackSessionKey(FeedbackSessionAttributes feedbackSession) { return String.format("%s-%s", feedbackSession.getCourseId(), feedbackSession.getFeedbackSessionName()); } - private String generatefeedbackSessionKey(String courseId, String feedbackSessionName) { + private String generateFeedbackSessionKey(String courseId, String feedbackSessionName) { return String.format("%s-%s", courseId, feedbackSessionName); } @@ -114,7 +127,7 @@ protected Account convert(AccountAttributes accAttr) { accAttr.getName(), accAttr.getEmail()); - UUID uuid = accountUuidGenerator.generateUuid(); + UUID uuid = ACCOUNT_UUID_GENERATOR.generateUuid(); sqlAccount.setId(uuid); accounts.put(accAttr.getGoogleId(), sqlAccount); @@ -128,13 +141,17 @@ protected Account convert(AccountAttributes accAttr) { protected AccountRequest convert(AccountRequestAttributes accReqAttr) { AccountRequest sqlAccountRequest = new AccountRequest(accReqAttr.getEmail(), accReqAttr.getName(), - accReqAttr.getInstitute()); + accReqAttr.getInstitute(), AccountRequestStatus.APPROVED, null); + + if (accReqAttr.getRegisteredAt() != null) { + sqlAccountRequest.setStatus(AccountRequestStatus.REGISTERED); + } sqlAccountRequest.setCreatedAt(accReqAttr.getCreatedAt()); sqlAccountRequest.setRegisteredAt(accReqAttr.getRegisteredAt()); sqlAccountRequest.setRegistrationKey(accReqAttr.getRegistrationKey()); - UUID uuid = accounRequestUuidGenerator.generateUuid(); + UUID uuid = ACCOUN_REQUEST_UUID_GENERATOR.generateUuid(); sqlAccountRequest.setId(uuid); return sqlAccountRequest; @@ -174,7 +191,7 @@ protected Notification convert(NotificationAttributes notifAttr) { sqlNotification.setShown(); } - UUID uuid = notificationUuidGenerator.generateUuid(); + UUID uuid = NOTIFICATION_UUID_GENERATOR.generateUuid(); sqlNotification.setId(uuid); notifications.put(notifAttr.getNotificationId(), sqlNotification); @@ -204,9 +221,9 @@ protected FeedbackSession convert(FeedbackSessionAttributes fsAttr) { sqlFs.setCreatedAt(fsAttr.getCreatedTime()); sqlFs.setDeletedAt(fsAttr.getDeletedTime()); - sqlFs.setId(feedbackSessionUuidGenerator.generateUuid()); + sqlFs.setId(FEEDBACK_SESSION_UUID_GENERATOR.generateUuid()); - feedbackSessions.put(generatefeedbackSessionKey(fsAttr), sqlFs); + feedbackSessions.put(generateFeedbackSessionKey(fsAttr), sqlFs); return sqlFs; } @@ -227,7 +244,7 @@ protected Instructor convert(InstructorAttributes instructor) { instructor.getDisplayedName(), role, instructor.getPrivileges()); - sqlInstructor.setId(instructorUuidGenerator.generateUuid()); + sqlInstructor.setId(INSTRUCTOR_UUID_GENERATOR.generateUuid()); sqlInstructor.setAccount(sqlAccount); return sqlInstructor; @@ -245,7 +262,7 @@ protected Student convert(StudentAttributes student) { student.getEmail(), student.getComments()); - sqlStudent.setId(studentUuidGenerator.generateUuid()); + sqlStudent.setId(STUDENT_UUID_GENERATOR.generateUuid()); sqlStudent.setAccount(sqlAccount); return sqlStudent; @@ -256,7 +273,7 @@ protected Student convert(StudentAttributes student) { */ protected DeadlineExtension convert(DeadlineExtensionAttributes deadlineExtension) { FeedbackSession sqlFeedbackSession = feedbackSessions.get( - generatefeedbackSessionKey(deadlineExtension.getCourseId(), deadlineExtension.getFeedbackSessionName())); + generateFeedbackSessionKey(deadlineExtension.getCourseId(), deadlineExtension.getFeedbackSessionName())); // User is not included since DataBundleLogic.java does not read users from this attribute DeadlineExtension sqlDE = new DeadlineExtension(null, @@ -265,7 +282,7 @@ protected DeadlineExtension convert(DeadlineExtensionAttributes deadlineExtensio sqlDE.setClosingSoonEmailSent(deadlineExtension.getSentClosingEmail()); sqlDE.setCreatedAt(deadlineExtension.getCreatedAt()); - sqlDE.setId(deadlineExtensionUuidGenerator.generateUuid()); + sqlDE.setId(DEADLINE_EXTENSION_UUID_GENERATOR.generateUuid()); return sqlDE; } @@ -275,7 +292,7 @@ protected DeadlineExtension convert(DeadlineExtensionAttributes deadlineExtensio */ protected FeedbackQuestion convert(FeedbackQuestionAttributes feedbackQuestion) { FeedbackSession sqlFeedbackSession = feedbackSessions.get( - generatefeedbackSessionKey(feedbackQuestion.getCourseId(), feedbackQuestion.getFeedbackSessionName())); + generateFeedbackSessionKey(feedbackQuestion.getCourseId(), feedbackQuestion.getFeedbackSessionName())); FeedbackQuestion sqlFq = FeedbackQuestion.makeQuestion(sqlFeedbackSession, feedbackQuestion.getQuestionNumber(), @@ -289,7 +306,7 @@ protected FeedbackQuestion convert(FeedbackQuestionAttributes feedbackQuestion) feedbackQuestion.getQuestionDetails()); sqlFq.setCreatedAt(feedbackQuestion.getCreatedAt()); - sqlFq.setId(feedbackQuestionUuidGenerator.generateUuid()); + sqlFq.setId(FEEDBACK_QUESTION_UUID_GENERATOR.generateUuid()); return sqlFq; } @@ -314,7 +331,7 @@ protected FeedbackResponse convert(FeedbackResponseAttributes feedbackResponse) sqlReceiverSection, feedbackResponse.getResponseDetails()); - sqlFeedbackResponse.setId(feedbackResponseUuidGenerator.generateUuid()); + sqlFeedbackResponse.setId(FEEDBACK_RESPONSE_UUID_GENERATOR.generateUuid()); sqlFeedbackResponse.setCreatedAt(feedbackResponse.getCreatedAt()); return sqlFeedbackResponse; @@ -355,7 +372,7 @@ protected Section createSection(StudentAttributes student) { Course sqlCourse = courses.get(student.getCourse()); Section sqlSection = new Section(sqlCourse, student.getSection()); - sqlSection.setId(sectionUuidGenerator.generateUuid()); + sqlSection.setId(SECTION_UUID_GENERATOR.generateUuid()); sections.put(generateSectionKey(student), sqlSection); @@ -368,7 +385,7 @@ protected Section createSection(StudentAttributes student) { protected Team createTeam(StudentAttributes student) { Section sqlSection = sections.get(generateSectionKey(student)); Team sqlTeam = new Team(sqlSection, student.getTeam()); - sqlTeam.setId(teamUuidGenerator.generateUuid()); + sqlTeam.setId(TEAM_UUID_GENERATOR.generateUuid()); return sqlTeam; } @@ -383,7 +400,7 @@ protected List createReadNotifications(AccountAttributes accou account.getReadNotifications().forEach((notifId, endTime) -> { Notification sqlNotification = notifications.get(notifId); ReadNotification sqlReadNotification = new ReadNotification(sqlAccount, sqlNotification); - sqlReadNotification.setId(readNotificationUuidGenerator.generateUuid()); + sqlReadNotification.setId(READ_NOTIFICATION_UUID_GENERATOR.generateUuid()); sqlReadNotifications.add(sqlReadNotification); }); diff --git a/src/e2e/java/teammates/e2e/cases/AdminHomePageE2ETest.java b/src/e2e/java/teammates/e2e/cases/AdminHomePageE2ETest.java index 6cfb6b478e00..014913e639c8 100644 --- a/src/e2e/java/teammates/e2e/cases/AdminHomePageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/AdminHomePageE2ETest.java @@ -5,7 +5,6 @@ import teammates.common.util.AppUrl; import teammates.common.util.Const; import teammates.e2e.pageobjects.AdminHomePage; -import teammates.storage.sqlentity.AccountRequest; /** * SUT: {@link Const.WebPageURIs#ADMIN_HOME_PAGE}. @@ -47,31 +46,6 @@ public void testAll() { String failureMessage = homePage.getMessageForInstructor(1); assertTrue(failureMessage.contains( "\"invalidemail\" is not acceptable to TEAMMATES as a/an email because it is not in the correct format.")); - - assertNotNull(BACKDOOR.getAccountRequest(email, institute)); - BACKDOOR.deleteAccountRequest(email, institute); - - ______TS("Failure case: Instructor is already registered"); - AccountRequest registeredAccountRequest = sqlTestData.accountRequests.get("AHome.instructor1OfCourse1"); - homePage.queueInstructorForAdding(registeredAccountRequest.getName(), - registeredAccountRequest.getEmail(), registeredAccountRequest.getInstitute()); - - homePage.addAllInstructors(); - - failureMessage = homePage.getMessageForInstructor(2); - assertTrue(failureMessage.contains("Cannot create account request as instructor has already registered.")); - - ______TS("Success case: Reset account request"); - - homePage.clickMoreInfoButtonForRegisteredInstructor(2); - homePage.clickResetAccountRequestLink(); - - successMessage = homePage.getMessageForInstructor(2); - assertTrue(successMessage.contains( - "Instructor \"" + registeredAccountRequest.getName() + "\" has been successfully created")); - - assertNull(BACKDOOR.getAccountRequest( - registeredAccountRequest.getEmail(), registeredAccountRequest.getInstitute()).getRegisteredAt()); } } diff --git a/src/e2e/java/teammates/e2e/cases/AdminSearchPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/AdminSearchPageE2ETest.java index b5ce80693f0d..22089ac7cead 100644 --- a/src/e2e/java/teammates/e2e/cases/AdminSearchPageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/AdminSearchPageE2ETest.java @@ -31,7 +31,7 @@ protected void prepareTestData() { putDocuments(testData); sqlTestData = loadSqlDataBundle("/AdminSearchPageE2ETest_SqlEntities.json"); removeAndRestoreSqlDataBundle(sqlTestData); - doPutDocumentsSql(sqlTestData); + putSqlDocuments(sqlTestData); } @Test @@ -132,7 +132,7 @@ public void testAll() { searchPage.inputSearchContent(searchContent); searchPage.clickSearchButton(); searchPage.clickResetAccountRequestButton(accountRequest); - assertNull(BACKDOOR.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()).getRegisteredAt()); + assertNull(BACKDOOR.getAccountRequest(accountRequest.getId()).getRegisteredAt()); ______TS("Typical case: Delete account request successful"); accountRequest = sqlTestData.accountRequests.get("unregisteredInstructor1"); @@ -141,7 +141,7 @@ public void testAll() { searchPage.inputSearchContent(searchContent); searchPage.clickSearchButton(); searchPage.clickDeleteAccountRequestButton(accountRequest); - assertNull(BACKDOOR.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute())); + assertNull(BACKDOOR.getAccountRequest(accountRequest.getId())); } private String getExpectedStudentDetails(StudentAttributes student) { @@ -193,7 +193,7 @@ private String getExpectedInstructorManageAccountLink(InstructorAttributes instr @AfterClass public void classTeardown() { for (AccountRequest request : sqlTestData.accountRequests.values()) { - BACKDOOR.deleteAccountRequest(request.getEmail(), request.getInstitute()); + BACKDOOR.deleteAccountRequest(request.getId()); } } diff --git a/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java b/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java index 3ada841ac59f..7b953f7a50a2 100644 --- a/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java +++ b/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java @@ -4,6 +4,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; +import java.util.UUID; import org.testng.ITestContext; import org.testng.annotations.AfterClass; @@ -329,7 +330,7 @@ protected String getKeyForStudent(StudentAttributes student) { @Override protected AccountRequestAttributes getAccountRequest(AccountRequestAttributes accountRequest) { - return BACKDOOR.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + return BACKDOOR.getAccountRequest(UUID.fromString(accountRequest.getId())); } NotificationAttributes getNotification(String notificationId) { diff --git a/src/e2e/java/teammates/e2e/cases/InstructorCourseJoinConfirmationPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/InstructorCourseJoinConfirmationPageE2ETest.java index d3dffb01c9f2..421144a2da42 100644 --- a/src/e2e/java/teammates/e2e/cases/InstructorCourseJoinConfirmationPageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/InstructorCourseJoinConfirmationPageE2ETest.java @@ -68,7 +68,7 @@ public void testAll() { ______TS("Click join link: valid account request key"); String regKey = BACKDOOR - .getRegKeyForAccountRequest("ICJoinConf.newinstr@gmail.tmt", "TEAMMATES Test Institute 1"); + .getRegKeyForAccountRequest(sqlTestData.accountRequests.get("ICJoinConf.newinstr").getId()); joinLink = createFrontendUrl(Const.WebPageURIs.JOIN_PAGE) .withIsCreatingAccount("true") diff --git a/src/e2e/java/teammates/e2e/cases/axe/AdminSearchPageAxeTest.java b/src/e2e/java/teammates/e2e/cases/axe/AdminSearchPageAxeTest.java index d45e1d4e16a5..732e06ceed31 100644 --- a/src/e2e/java/teammates/e2e/cases/axe/AdminSearchPageAxeTest.java +++ b/src/e2e/java/teammates/e2e/cases/axe/AdminSearchPageAxeTest.java @@ -25,7 +25,7 @@ protected void prepareTestData() { putDocuments(testData); sqlTestData = loadSqlDataBundle("/AdminSearchPageE2ETest_SqlEntities.json"); removeAndRestoreSqlDataBundle(sqlTestData); - doPutDocumentsSql(sqlTestData); + putSqlDocuments(sqlTestData); } @Test diff --git a/src/e2e/java/teammates/e2e/cases/axe/InstructorFeedbackEditPageAxeTest.java b/src/e2e/java/teammates/e2e/cases/axe/InstructorFeedbackEditPageAxeTest.java index da7e7ca4cb14..79c96f478f6b 100644 --- a/src/e2e/java/teammates/e2e/cases/axe/InstructorFeedbackEditPageAxeTest.java +++ b/src/e2e/java/teammates/e2e/cases/axe/InstructorFeedbackEditPageAxeTest.java @@ -33,8 +33,8 @@ public void testAll() { testData.instructors.get("instructor").getGoogleId()); // landmark-unique might be caused by tinymce - // aria-allowed-attr is caused by https://github.com/tinymce/tinymce/issues/7346 - Results results = getAxeBuilder("aria-allowed-attr", "landmark-unique") + // aria-prohibited-attr is caused by https://github.com/tinymce/tinymce/issues/7346 + Results results = getAxeBuilder("aria-prohibited-attr", "landmark-unique") .analyze(feedbackEditPage.getBrowser().getDriver()); assertTrue(formatViolations(results), results.violationFree()); } diff --git a/src/e2e/java/teammates/e2e/cases/sql/AdminHomePageE2ETest.java b/src/e2e/java/teammates/e2e/cases/sql/AdminHomePageE2ETest.java new file mode 100644 index 000000000000..e4e4d8c35601 --- /dev/null +++ b/src/e2e/java/teammates/e2e/cases/sql/AdminHomePageE2ETest.java @@ -0,0 +1,55 @@ +package teammates.e2e.cases.sql; + +import org.testng.annotations.Test; + +import teammates.common.util.AppUrl; +import teammates.common.util.Const; +import teammates.e2e.pageobjects.AdminHomePage; + +/** + * SUT: {@link Const.WebPageURIs#ADMIN_HOME_PAGE}. + */ +public class AdminHomePageE2ETest extends BaseE2ETestCase { + + @Override + protected void prepareTestData() { + // not needed + } + + @Test + @Override + public void testAll() { + AppUrl url = createFrontendUrl(Const.WebPageURIs.ADMIN_HOME_PAGE); + AdminHomePage homePage = loginAdminToPage(url, AdminHomePage.class); + + ______TS("Test adding instructors with both valid and invalid details"); + + String name = "AHPUiT InstrĂºctör WithPlusInEmail"; + String email = "AHPUiT+++_.instr1!@gmail.tmt"; + String institute = "TEAMMATES Test Institute 1"; + + homePage.queueInstructorForAdding(name, email, institute); + + String singleLineDetails = "Instructor With Invalid Email | invalidemail | TEAMMATES Test Institute 1"; + + homePage.queueInstructorForAdding(singleLineDetails); + + homePage.addAllInstructors(); + + String successMessage = homePage.getMessageForInstructor(0); + assertTrue(successMessage.contains( + "Instructor \"AHPUiT InstrĂºctör WithPlusInEmail\" has been successfully created")); + + String failureMessage = homePage.getMessageForInstructor(1); + assertTrue(failureMessage.contains( + "\"invalidemail\" is not acceptable to TEAMMATES as a/an email because it is not in the correct format.")); + + homePage.reloadPage(); + + ______TS("Verify that newly added instructor appears in account request table"); + + homePage.verifyInstructorInAccountRequestTable(name, email, institute); + + } + +} diff --git a/src/e2e/java/teammates/e2e/cases/sql/AdminSearchPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/sql/AdminSearchPageE2ETest.java new file mode 100644 index 000000000000..eb209578a7d4 --- /dev/null +++ b/src/e2e/java/teammates/e2e/cases/sql/AdminSearchPageE2ETest.java @@ -0,0 +1,282 @@ +package teammates.e2e.cases.sql; + +import java.time.Instant; + +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.util.AppUrl; +import teammates.common.util.Const; +import teammates.common.util.FieldValidator; +import teammates.common.util.StringHelperExtension; +import teammates.e2e.pageobjects.AdminSearchPage; +import teammates.e2e.util.TestProperties; +import teammates.storage.sqlentity.AccountRequest; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.Instructor; +import teammates.storage.sqlentity.Student; + +/** + * SUT: {@link Const.WebPageURIs#ADMIN_SEARCH_PAGE}. + */ +public class AdminSearchPageE2ETest extends BaseE2ETestCase { + + @Override + protected void prepareTestData() { + if (!TestProperties.INCLUDE_SEARCH_TESTS) { + return; + } + testData = removeAndRestoreDataBundle(loadSqlDataBundle("/AdminSearchPageE2ESqlTest.json")); + putDocuments(testData); + } + + @Test + @Override + public void testAll() { + if (!TestProperties.INCLUDE_SEARCH_TESTS) { + return; + } + + AppUrl url = createFrontendUrl(Const.WebPageURIs.ADMIN_SEARCH_PAGE); + AdminSearchPage searchPage = loginAdminToPage(url, AdminSearchPage.class); + + Course course = testData.courses.get("typicalCourse1"); + Student student = testData.students.get("student1InCourse1"); + Instructor instructor = testData.instructors.get("instructor1OfCourse1"); + AccountRequest accountRequest = testData.accountRequests.get("instructor1OfCourse1"); + + ______TS("Typical case: Search student email"); + String searchContent = student.getEmail(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + String studentDetails = getExpectedStudentDetails(student); + String studentManageAccountLink = getExpectedStudentManageAccountLink(student); + String studentHomePageLink = getExpectedStudentHomePageLink(student); + int numExpandedRows = getExpectedNumExpandedRows(student); + searchPage.verifyStudentRowContent(student, course, studentDetails, studentManageAccountLink, + studentHomePageLink); + searchPage.verifyStudentExpandedLinks(student, numExpandedRows); + + ______TS("Typical case: Reset student google id"); + searchPage.resetStudentGoogleId(student); + student.setGoogleId(null); + searchPage.verifyStudentRowContentAfterReset(student, course); + + ______TS("Typical case: Regenerate registration key for a course student"); + searchPage.clickExpandStudentLinks(); + String originalJoinLink = searchPage.getStudentJoinLink(student); + searchPage.regenerateStudentKey(student); + searchPage.verifyRegenerateStudentKey(student, originalJoinLink); + searchPage.waitForPageToLoad(); + + ______TS("Typical case: Search for instructor email"); + searchPage.clearSearchBox(); + searchContent = instructor.getEmail(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + String instructorManageAccountLink = getExpectedInstructorManageAccountLink(instructor); + String instructorHomePageLink = getExpectedInstructorHomePageLink(instructor); + searchPage.verifyInstructorRowContent(instructor, course, instructorManageAccountLink, + instructorHomePageLink); + searchPage.verifyInstructorExpandedLinks(instructor); + + ______TS("Typical case: Reset instructor google id"); + searchPage.resetInstructorGoogleId(instructor); + searchPage.verifyInstructorRowContentAfterReset(instructor, course); + + ______TS("Typical case: Regenerate registration key for an instructor"); + searchPage.clickExpandInstructorLinks(); + originalJoinLink = searchPage.getInstructorJoinLink(instructor); + searchPage.regenerateInstructorKey(instructor); + searchPage.verifyRegenerateInstructorKey(instructor, originalJoinLink); + searchPage.waitForPageToLoad(); + + ______TS("Typical case: Search for account request by email"); + searchPage.clearSearchBox(); + searchContent = accountRequest.getEmail(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.verifyAccountRequestRowContent(accountRequest); + searchPage.verifyAccountRequestExpandedLinks(accountRequest); + + ______TS("Typical case: Search common search key"); + searchPage.clearSearchBox(); + searchContent = "Course1"; + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.verifyStudentRowContentAfterReset(student, course); + searchPage.verifyInstructorRowContentAfterReset(instructor, course); + searchPage.verifyAccountRequestRowContent(accountRequest); + + ______TS("Typical case: Expand and collapse links"); + searchPage.verifyLinkExpansionButtons(student, instructor, accountRequest); + + ______TS("Typical case: Reset account request successful"); + searchContent = "ASearch.instructor1@gmail.tmt"; + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickResetAccountRequestButton(accountRequest); + assertNull(BACKDOOR.getAccountRequest(accountRequest.getId()).getRegisteredAt()); + + ______TS("Typical case: Delete account request successful"); + accountRequest = testData.accountRequests.get("unregisteredInstructor1"); + searchContent = accountRequest.getEmail(); + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickDeleteAccountRequestButton(accountRequest); + assertNull(BACKDOOR.getAccountRequest(accountRequest.getId())); + + ______TS("Typical case: Edit account request successful"); + accountRequest = testData.accountRequests.get("unregisteredInstructor2"); + searchContent = accountRequest.getEmail(); + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickEditAccountRequestButton(accountRequest); + searchPage.fillInEditModalFields("Different name", accountRequest.getEmail(), + accountRequest.getInstitute(), "New comment"); + searchPage.clickSaveEditAccountRequestButton(); + accountRequest.setName("Different name"); + accountRequest.setComments("New comment"); + searchPage.verifyAccountRequestRowContent(accountRequest); + + ______TS("Typical case: View comment of account request"); + accountRequest = testData.accountRequests.get("unregisteredInstructor2"); + searchContent = accountRequest.getEmail(); + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickViewAccountRequestAndVerifyCommentsButton(accountRequest, "New comment"); + + ______TS("Edit account request with invalid details"); + accountRequest = testData.accountRequests.get("unregisteredInstructor2"); + searchContent = accountRequest.getEmail(); + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickEditAccountRequestButton(accountRequest); + searchPage.fillInEditModalFields(accountRequest.getName(), "invalid", + accountRequest.getInstitute(), "New comment"); + searchPage.clickSaveEditAccountRequestButton(); + String formattedErrorMessage = String.format("\"%s\" is not acceptable to TEAMMATES as a/an %s because it %s. " + + "An email address contains some text followed by one '@' sign followed by some more text, " + + "and should end with a top level domain address like .com. It cannot be longer than %d characters, " + + "cannot be empty and cannot contain spaces.", + "invalid", FieldValidator.EMAIL_FIELD_NAME, FieldValidator.REASON_INCORRECT_FORMAT, + FieldValidator.EMAIL_MAX_LENGTH); + searchPage.verifyStatusMessage(formattedErrorMessage); + + String name = StringHelperExtension.generateStringOfLength(FieldValidator.PERSON_NAME_MAX_LENGTH + 1); + + searchPage.clickEditAccountRequestButton(accountRequest); + searchPage.fillInEditModalFields(name, accountRequest.getEmail(), accountRequest.getInstitute(), "New comment"); + searchPage.clickSaveEditAccountRequestButton(); + formattedErrorMessage = String.format("\"%s\" is not acceptable to TEAMMATES as a/an %s because it %s. " + + "The value of a/an %s should be no longer than %d characters. It should not be empty.", + name, FieldValidator.PERSON_NAME_FIELD_NAME, FieldValidator.REASON_TOO_LONG, + FieldValidator.PERSON_NAME_FIELD_NAME, FieldValidator.PERSON_NAME_MAX_LENGTH); + searchPage.verifyStatusMessage(formattedErrorMessage); + + ______TS("Typical case: Approve account request successful"); + accountRequest = testData.accountRequests.get("unregisteredInstructor2"); + searchContent = accountRequest.getEmail(); + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickApproveAccountRequestButton(accountRequest); + accountRequest.setStatus(AccountRequestStatus.APPROVED); + searchPage.verifyAccountRequestRowContent(accountRequest); + + ______TS("Typical case: Reject account request successfully"); + accountRequest = testData.accountRequests.get("unregisteredInstructor3"); + searchContent = accountRequest.getEmail(); + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickRejectAccountRequestButton(accountRequest); + accountRequest.setStatus(AccountRequestStatus.REJECTED); + searchPage.verifyAccountRequestRowContent(accountRequest); + + ______TS("Reject account request with empty body"); + accountRequest = testData.accountRequests.get("unregisteredInstructor5"); + searchContent = accountRequest.getEmail(); + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickRejectAccountRequestWithReasonButton(accountRequest); + searchPage.fillInRejectionModalBody(""); + searchPage.clickConfirmRejectAccountRequest(); + searchPage.verifyStatusMessage("Please provide an email body for the rejection email."); + searchPage.closeRejectionModal(); + + ______TS("Typical case: Reject account request with reason successfully"); + accountRequest = testData.accountRequests.get("unregisteredInstructor4"); + searchContent = accountRequest.getEmail(); + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickRejectAccountRequestWithReasonButton(accountRequest); + accountRequest.setStatus(AccountRequestStatus.REJECTED); + searchPage.verifyAccountRequestRowContent(accountRequest); + } + + private String getExpectedStudentDetails(Student student) { + return String.format("%s [%s] (%s)", student.getCourse().getId(), + student.getSection() == null + ? Const.DEFAULT_SECTION + : student.getSection().getName(), + student.getTeam().getName()); + } + + private String getExpectedStudentHomePageLink(Student student) { + return student.isRegistered() ? createFrontendUrl(Const.WebPageURIs.STUDENT_HOME_PAGE) + .withUserId(student.getGoogleId()) + .toAbsoluteString() + : ""; + } + + private String getExpectedStudentManageAccountLink(Student student) { + return student.isRegistered() ? createFrontendUrl(Const.WebPageURIs.ADMIN_ACCOUNTS_PAGE) + .withParam(Const.ParamsNames.INSTRUCTOR_ID, student.getGoogleId()) + .toAbsoluteString() + : ""; + } + + private int getExpectedNumExpandedRows(Student student) { + int expectedNumExpandedRows = 2; + for (FeedbackSession sessions : testData.feedbackSessions.values()) { + if (sessions.getCourse().equals(student.getCourse())) { + expectedNumExpandedRows += 1; + if (sessions.getResultsVisibleFromTime().isBefore(Instant.now())) { + expectedNumExpandedRows += 1; + } + } + } + return expectedNumExpandedRows; + } + + private String getExpectedInstructorHomePageLink(Instructor instructor) { + String googleId = instructor.isRegistered() ? instructor.getGoogleId() : ""; + return createFrontendUrl(Const.WebPageURIs.INSTRUCTOR_HOME_PAGE) + .withUserId(googleId) + .toAbsoluteString(); + } + + private String getExpectedInstructorManageAccountLink(Instructor instructor) { + String googleId = instructor.isRegistered() ? instructor.getGoogleId() : ""; + return createFrontendUrl(Const.WebPageURIs.ADMIN_ACCOUNTS_PAGE) + .withParam(Const.ParamsNames.INSTRUCTOR_ID, googleId) + .toAbsoluteString(); + } + + @AfterClass + public void classTeardown() { + for (AccountRequest request : testData.accountRequests.values()) { + BACKDOOR.deleteAccountRequest(request.getId()); + } + } +} diff --git a/src/e2e/java/teammates/e2e/cases/sql/BaseE2ETestCase.java b/src/e2e/java/teammates/e2e/cases/sql/BaseE2ETestCase.java index b61a4a8cf2ac..6878da94e444 100644 --- a/src/e2e/java/teammates/e2e/cases/sql/BaseE2ETestCase.java +++ b/src/e2e/java/teammates/e2e/cases/sql/BaseE2ETestCase.java @@ -22,12 +22,15 @@ import teammates.e2e.util.TestProperties; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackResponse; +import teammates.storage.sqlentity.FeedbackSession; import teammates.storage.sqlentity.Student; import teammates.test.BaseTestCaseWithSqlDatabaseAccess; import teammates.test.FileHelper; import teammates.test.ThreadHelper; import teammates.ui.output.FeedbackQuestionData; import teammates.ui.output.FeedbackResponseData; +import teammates.ui.output.FeedbackSessionData; +import teammates.ui.output.FeedbackSessionPublishStatus; import teammates.ui.output.StudentData; /** @@ -265,4 +268,38 @@ StudentData getStudent(String courseId, String studentEmailAddress) { protected StudentData getStudent(Student student) { return getStudent(student.getCourseId(), student.getEmail()); } + + FeedbackSessionData getFeedbackSession(String courseId, String feedbackSessionName) { + return BACKDOOR.getFeedbackSessionData(courseId, feedbackSessionName); + } + + @Override + protected FeedbackSessionData getFeedbackSession(FeedbackSession feedbackSession) { + return getFeedbackSession(feedbackSession.getCourse().getId(), feedbackSession.getName()); + } + + /** + * Checks if the feedback session is published. + */ + protected boolean isFeedbackSessionPublished(FeedbackSessionPublishStatus status) { + return status == FeedbackSessionPublishStatus.PUBLISHED; + } + + FeedbackSessionData getSoftDeletedSession(String feedbackSessionName, String instructorId) { + return BACKDOOR.getSoftDeletedSessionData(feedbackSessionName, instructorId); + } + + /** + * Puts the documents in the database using BACKDOOR. + * @param dataBundle the data to be put in the database + * @return the result of the operation + */ + protected String putDocuments(SqlDataBundle dataBundle) { + try { + return BACKDOOR.putSqlDocuments(dataBundle); + } catch (HttpRequestFailedException e) { + e.printStackTrace(); + return null; + } + } } diff --git a/src/e2e/java/teammates/e2e/cases/sql/FeedbackNumScaleQuestionE2ETest.java b/src/e2e/java/teammates/e2e/cases/sql/FeedbackNumScaleQuestionE2ETest.java new file mode 100644 index 000000000000..61e4fc619bed --- /dev/null +++ b/src/e2e/java/teammates/e2e/cases/sql/FeedbackNumScaleQuestionE2ETest.java @@ -0,0 +1,124 @@ +package teammates.e2e.cases.sql; + +import org.testng.annotations.Test; + +import teammates.common.datatransfer.questions.FeedbackNumericalScaleQuestionDetails; +import teammates.common.datatransfer.questions.FeedbackNumericalScaleResponseDetails; +import teammates.e2e.pageobjects.FeedbackSubmitPage; +import teammates.e2e.pageobjects.InstructorFeedbackEditPage; +import teammates.storage.sqlentity.FeedbackQuestion; +import teammates.storage.sqlentity.FeedbackResponse; +import teammates.storage.sqlentity.Student; + +/** + * SUT: {@link Const.WebPageURIs#INSTRUCTOR_SESSION_EDIT_PAGE}, {@link Const.WebPageURIs#SESSION_SUBMISSION_PAGE} + * specifically for NumScale questions. + */ +public class FeedbackNumScaleQuestionE2ETest extends BaseFeedbackQuestionE2ETest { + + @Override + protected void prepareTestData() { + testData = removeAndRestoreDataBundle(loadSqlDataBundle("/FeedbackNumScaleQuestionE2ESqlTest.json")); + + instructor = testData.instructors.get("instructor"); + course = testData.courses.get("course"); + feedbackSession = testData.feedbackSessions.get("openSession"); + student = testData.students.get("alice.tmms@FNumScaleQn.CS2104"); + } + + @Test + @Override + public void testAll() { + testEditPage(); + logout(); + testSubmitPage(); + } + + @Override + protected void testEditPage() { + InstructorFeedbackEditPage feedbackEditPage = loginToFeedbackEditPage(); + + ______TS("verify loaded question"); + FeedbackQuestion loadedQuestion = testData.feedbackQuestions.get("qn1ForFirstSession"); + FeedbackNumericalScaleQuestionDetails questionDetails = + (FeedbackNumericalScaleQuestionDetails) loadedQuestion.getQuestionDetailsCopy(); + feedbackEditPage.verifyNumScaleQuestionDetails(1, questionDetails); + + ______TS("add new question"); + // add new question exactly like loaded question + loadedQuestion.setQuestionNumber(2); + feedbackEditPage.addNumScaleQuestion(loadedQuestion); + feedbackEditPage.waitUntilAnimationFinish(); + + feedbackEditPage.verifyNumScaleQuestionDetails(2, questionDetails); + verifyPresentInDatabase(loadedQuestion); + + ______TS("copy question"); + FeedbackQuestion copiedQuestion = testData.feedbackQuestions.get("qn1ForSecondSession"); + questionDetails = (FeedbackNumericalScaleQuestionDetails) copiedQuestion.getQuestionDetailsCopy(); + feedbackEditPage.copyQuestion(copiedQuestion.getCourseId(), + copiedQuestion.getQuestionDetailsCopy().getQuestionText()); + copiedQuestion.setQuestionNumber(3); + copiedQuestion.setFeedbackSession(feedbackSession); + + feedbackEditPage.verifyNumScaleQuestionDetails(3, questionDetails); + verifyPresentInDatabase(copiedQuestion); + + ______TS("edit question"); + questionDetails = (FeedbackNumericalScaleQuestionDetails) loadedQuestion.getQuestionDetailsCopy(); + FeedbackNumericalScaleQuestionDetails newQuestionDetails = + (FeedbackNumericalScaleQuestionDetails) questionDetails.getDeepCopy(); + newQuestionDetails.setMinScale(0); + newQuestionDetails.setStep(1); + newQuestionDetails.setMaxScale(100); + loadedQuestion.setQuestionDetails(newQuestionDetails); + feedbackEditPage.editNumScaleQuestion(2, newQuestionDetails); + feedbackEditPage.waitForPageToLoad(); + + feedbackEditPage.verifyNumScaleQuestionDetails(2, newQuestionDetails); + verifyPresentInDatabase(loadedQuestion); + + // reset question details to original + loadedQuestion.setQuestionDetails(questionDetails); + } + + @Override + protected void testSubmitPage() { + FeedbackSubmitPage feedbackSubmitPage = loginToFeedbackSubmitPage(); + + ______TS("verify loaded question"); + FeedbackQuestion question = testData.feedbackQuestions.get("qn1ForFirstSession"); + Student receiver = testData.students.get("benny.tmms@FNumScaleQn.CS2104"); + feedbackSubmitPage.verifyNumScaleQuestion(1, receiver.getTeamName(), + (FeedbackNumericalScaleQuestionDetails) question.getQuestionDetailsCopy()); + + ______TS("submit response"); + FeedbackResponse response = getResponse(question, receiver, 5.4); + feedbackSubmitPage.fillNumScaleResponse(1, receiver.getTeamName(), response); + feedbackSubmitPage.clickSubmitQuestionButton(1); + + // TODO: uncomment when SubmitFeedbackResponse is working + // verifyPresentInDatabase(response); + + // ______TS("check previous response"); + // feedbackSubmitPage = getFeedbackSubmitPage(); + // feedbackSubmitPage.verifyNumScaleResponse(1, receiver.getTeamName(), response); + + // ______TS("edit response"); + // response = getResponse(question, receiver, 10.0); + // feedbackSubmitPage.fillNumScaleResponse(1, receiver.getTeamName(), response); + // feedbackSubmitPage.clickSubmitQuestionButton(1); + + // feedbackSubmitPage = getFeedbackSubmitPage(); + // feedbackSubmitPage.verifyNumScaleResponse(1, receiver.getTeamName(), response); + // verifyPresentInDatabase(response); + } + + private FeedbackResponse getResponse(FeedbackQuestion feedbackQuestion, Student receiver, Double answer) { + FeedbackNumericalScaleResponseDetails details = new FeedbackNumericalScaleResponseDetails(); + details.setAnswer(answer); + return FeedbackResponse.makeResponse( + feedbackQuestion, student.getEmail(), null, receiver.getTeamName(), null, details); + } + +} diff --git a/src/e2e/java/teammates/e2e/cases/sql/InstructorHomePageE2ETest.java b/src/e2e/java/teammates/e2e/cases/sql/InstructorHomePageE2ETest.java new file mode 100644 index 000000000000..239de4ce25c1 --- /dev/null +++ b/src/e2e/java/teammates/e2e/cases/sql/InstructorHomePageE2ETest.java @@ -0,0 +1,261 @@ +package teammates.e2e.cases.sql; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import teammates.common.util.AppUrl; +import teammates.common.util.Const; +import teammates.e2e.pageobjects.InstructorHomePage; +import teammates.e2e.util.TestProperties; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.Instructor; +import teammates.storage.sqlentity.Student; +import teammates.test.ThreadHelper; +import teammates.ui.output.FeedbackSessionData; + +/** + * SUT: {@link Const.WebPageURIs#INSTRUCTOR_HOME_PAGE}. + */ +public class InstructorHomePageE2ETest extends BaseE2ETestCase { + private Instructor instructor; + private Student studentToEmail; + private Course course; + private Course otherCourse; + + private FeedbackSession feedbackSessionAwaiting; + private FeedbackSession feedbackSessionOpen; + private FeedbackSession feedbackSessionClosed; + private FeedbackSession feedbackSessionPublished; + private FeedbackSession otherCourseSession; + + private String fileName; + + @Override + protected void prepareTestData() { + testData = loadSqlDataBundle("/InstructorHomePageE2ETestSql.json"); + studentToEmail = testData.students.get("IHome.charlie.d.tmms@IHome.CS2104"); + studentToEmail.setEmail(TestProperties.TEST_EMAIL); + testData = removeAndRestoreDataBundle(testData); + putDocuments(testData); + + instructor = testData.instructors.get("IHome.instr.CS2104"); + course = testData.courses.get("IHome.CS2104"); + otherCourse = testData.courses.get("IHome.CS1101"); + + feedbackSessionAwaiting = testData.feedbackSessions.get("Second Feedback Session"); + feedbackSessionOpen = testData.feedbackSessions.get("First Feedback Session"); + feedbackSessionClosed = testData.feedbackSessions.get("Third Feedback Session"); + feedbackSessionPublished = testData.feedbackSessions.get("Fourth Feedback Session"); + otherCourseSession = testData.feedbackSessions.get("CS1101 Session"); + + fileName = "/" + feedbackSessionOpen.getCourse().getId() + "_" + feedbackSessionOpen.getName() + + "_result.csv"; + } + + @BeforeClass + public void classSetup() { + deleteDownloadsFile(fileName); + } + + @Test + @Override + public void testAll() { + AppUrl url = createFrontendUrl(Const.WebPageURIs.INSTRUCTOR_HOME_PAGE); + InstructorHomePage homePage = loginToPage(url, InstructorHomePage.class, instructor.getGoogleId()); + + ______TS("verify loaded data"); + homePage.sortCoursesById(); + int courseIndex = 1; + int otherCourseIndex = 0; + // by default, sessions are sorted by end date in descending order + FeedbackSession[] courseSessions = { feedbackSessionOpen, feedbackSessionAwaiting, + feedbackSessionClosed, feedbackSessionPublished }; + FeedbackSession[] otherCourseSessions = { otherCourseSession }; + // use course index instead of searching for course in table to test sorted order of courses + homePage.verifyCourseTabDetails(otherCourseIndex, otherCourse, otherCourseSessions); + homePage.verifyCourseTabDetails(courseIndex, course, courseSessions); + + ______TS("notification banner is visible"); + assertTrue(homePage.isBannerVisible()); + + ______TS("verify response rate"); + for (int i = 0; i < courseSessions.length; i++) { + homePage.verifyResponseRate(courseIndex, i, getExpectedResponseRate(courseSessions[i])); + } + + ______TS("copy session with modified session timings"); + int sessionIndex = 1; + String newName = "Copied Name"; + FeedbackSession copiedSession = feedbackSessionAwaiting.getCopy(); + copiedSession.setCourse(otherCourse); + copiedSession.setName(newName); + copiedSession.setCreatedAt(Instant.now()); + int startHour = ZonedDateTime.ofInstant(copiedSession.getStartTime(), + ZoneId.of(copiedSession.getCourse().getTimeZone())).getHour(); + copiedSession.setStartTime(ZonedDateTime.now(ZoneId.of(otherCourse.getTimeZone())).plus(Duration.ofDays(2)) + .withHour(startHour).truncatedTo(ChronoUnit.HOURS).toInstant()); + int endHour = ZonedDateTime.ofInstant(copiedSession.getEndTime(), ZoneId.of(copiedSession.getCourse().getTimeZone())) + .getHour(); + copiedSession.setEndTime(ZonedDateTime.now(ZoneId.of(otherCourse.getTimeZone())).plus(Duration.ofDays(7)) + .withHour(endHour).truncatedTo(ChronoUnit.HOURS).toInstant()); + copiedSession.setSessionVisibleFromTime(ZonedDateTime.now(ZoneId.of(otherCourse.getTimeZone())) + .minus(Duration.ofDays(28)).withHour(startHour).truncatedTo(ChronoUnit.HOURS).toInstant()); + copiedSession.setResultsVisibleFromTime(Const.TIME_REPRESENTS_LATER); + homePage.copySession(courseIndex, sessionIndex, otherCourse, newName); + + homePage.waitForConfirmationModalAndClickOk(); + homePage = getNewPageInstance(url, InstructorHomePage.class); + homePage.sortCoursesByName(); + // flip index after sorting + courseIndex = 0; + otherCourseIndex = 1; + FeedbackSession[] otherCourseSessionsWithCopy = { copiedSession, otherCourseSession }; + homePage.verifyCourseTabDetails(otherCourseIndex, otherCourse, otherCourseSessionsWithCopy); + verifyPresentInDatabase(copiedSession); + + ______TS("copy session with same session timings"); + sessionIndex = 0; + newName = "Copied Name 2"; + FeedbackSession copiedSession2 = copiedSession.getCopy(); + copiedSession2.setName(newName); + copiedSession2.setCreatedAt(Instant.now()); + homePage.copySession(otherCourseIndex, sessionIndex, otherCourse, newName); + + homePage.verifyStatusMessage("The feedback session has been copied. " + + "Please modify settings/questions as necessary."); + homePage = getNewPageInstance(url, InstructorHomePage.class); + homePage.sortCoursesByName(); + FeedbackSession[] otherCourseSessionsWithTwoCopies = { copiedSession, copiedSession2, otherCourseSession }; + homePage.verifyCourseTabDetails(otherCourseIndex, otherCourse, otherCourseSessionsWithTwoCopies); + verifyPresentInDatabase(copiedSession2); + + ______TS("publish results"); + sessionIndex = 0; + feedbackSessionOpen.setResultsVisibleFromTime(Const.TIME_REPRESENTS_NOW); + homePage.publishSessionResults(courseIndex, sessionIndex); + + homePage.verifyStatusMessage("The feedback session has been published. " + + "Please allow up to 1 hour for all the notification emails to be sent out."); + homePage.verifySessionDetails(courseIndex, sessionIndex, feedbackSessionOpen); + verifySessionPublishedState(feedbackSessionOpen, true); + verifyEmailSent(studentToEmail.getEmail(), "TEAMMATES: Feedback session results published" + + " [Course: " + course.getName() + "][Feedback Session: " + + feedbackSessionOpen.getName() + "]"); + + ______TS("send reminder email to selected student"); + homePage.sendReminderEmailToSelectedStudent(courseIndex, sessionIndex, studentToEmail); + + homePage.verifyStatusMessage("Reminder e-mails have been sent out to those students" + + " and instructors. Please allow up to 1 hour for all the notification emails to be sent out."); + verifyEmailSent(studentToEmail.getEmail(), "TEAMMATES: Feedback session reminder" + + " [Course: " + course.getName() + "][Feedback Session: " + + feedbackSessionOpen.getName() + "]"); + + ______TS("send reminder email to all student non-submitters"); + homePage.sendReminderEmailToNonSubmitters(courseIndex, sessionIndex); + + homePage.verifyStatusMessage("Reminder e-mails have been sent out to those students" + + " and instructors. Please allow up to 1 hour for all the notification emails to be sent out."); + verifyEmailSent(studentToEmail.getEmail(), "TEAMMATES: Feedback session reminder" + + " [Course: " + course.getName() + "][Feedback Session: " + + feedbackSessionOpen.getName() + "]"); + ______TS("resend results link"); + homePage.resendResultsLink(courseIndex, sessionIndex, studentToEmail); + + homePage.verifyStatusMessage("Session published notification emails have been resent" + + " to those students and instructors. Please allow up to 1 hour for all the notification emails to be" + + " sent out."); + verifyEmailSent(studentToEmail.getEmail(), "TEAMMATES: Feedback session results published" + + " [Course: " + course.getName() + "][Feedback Session: " + + feedbackSessionOpen.getName() + "]"); + + ______TS("unpublish results"); + feedbackSessionOpen.setResultsVisibleFromTime(Const.TIME_REPRESENTS_LATER); + homePage.unpublishSessionResults(courseIndex, sessionIndex); + + homePage.verifyStatusMessage("The feedback session has been unpublished."); + homePage.verifySessionDetails(courseIndex, sessionIndex, feedbackSessionOpen); + verifySessionPublishedState(feedbackSessionOpen, false); + verifyEmailSent(studentToEmail.getEmail(), "TEAMMATES: Feedback session results unpublished" + + " [Course: " + course.getName() + "][Feedback Session: " + + feedbackSessionOpen.getName() + "]"); + + ______TS("download results"); + homePage.downloadResults(courseIndex, sessionIndex); + List expectedContent = Arrays.asList("Course,tm.e2e.IHome.CS2104", + "Session Name,First Feedback Session", "Question 1,Rate 5 other students' products"); + verifyDownloadedFile(fileName, expectedContent); + + ______TS("soft delete session"); + sessionIndex = 1; + copiedSession.setDeletedAt(Instant.now()); + homePage.deleteSession(otherCourseIndex, sessionIndex); + + homePage.verifyStatusMessage("The feedback session has been deleted. " + + "You can restore it from the 'Sessions' tab."); + homePage.sortCoursesByName(); + otherCourseIndex = 1; + FeedbackSession[] otherCourseSessionsWithCopyTwo = { copiedSession, otherCourseSession }; + homePage.verifyCourseTabDetails(otherCourseIndex, otherCourse, otherCourseSessionsWithCopyTwo); + assertNotNull(getSoftDeletedSession(copiedSession2.getName(), + instructor.getGoogleId())); + + ______TS("delete course"); + otherCourseIndex = 1; + homePage.deleteCourse(otherCourseIndex); + + homePage.verifyStatusMessage("The course " + otherCourse.getId() + " has been deleted. " + + "You can restore it from the Recycle Bin manually."); + homePage.verifyNumCourses(1); + assertTrue(BACKDOOR.isCourseInRecycleBin(otherCourse.getId())); + } + + private String getExpectedResponseRate(FeedbackSession session) { + String sessionName = session.getName(); + boolean hasQuestion = testData.feedbackQuestions.values() + .stream() + .anyMatch(q -> q.getFeedbackSessionName().equals(sessionName)); + + if (!hasQuestion) { + return "0 / 0"; + } + + long numStudents = testData.students.values() + .stream() + .filter(s -> s.getCourse().getId().equals(session.getCourse().getId())) + .count(); + + Set uniqueGivers = new HashSet<>(); + testData.feedbackResponses.values() + .stream() + .filter(r -> r.getFeedbackQuestion().getFeedbackSessionName().equals(sessionName)) + .forEach(r -> uniqueGivers.add(r.getGiver())); + int numResponses = uniqueGivers.size(); + + return numResponses + " / " + numStudents; + } + + private void verifySessionPublishedState(FeedbackSession feedbackSession, boolean state) { + int retryLimit = 5; + FeedbackSessionData actual = getFeedbackSession(feedbackSession.getCourse().getId(), + feedbackSession.getName()); + while (isFeedbackSessionPublished(actual.getPublishStatus()) != state && retryLimit > 0) { + retryLimit--; + ThreadHelper.waitFor(1000); + actual = getFeedbackSession(feedbackSession.getCourse().getId(), + feedbackSession.getName()); + } + assertEquals(isFeedbackSessionPublished(actual.getPublishStatus()), state); + } +} diff --git a/src/e2e/java/teammates/e2e/cases/sql/InstructorStudentListPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/sql/InstructorStudentListPageE2ETest.java new file mode 100644 index 000000000000..b16564db651a --- /dev/null +++ b/src/e2e/java/teammates/e2e/cases/sql/InstructorStudentListPageE2ETest.java @@ -0,0 +1,123 @@ +package teammates.e2e.cases.sql; + +import java.util.HashMap; +import java.util.Map; + +import org.testng.annotations.Test; + +import teammates.common.util.AppUrl; +import teammates.common.util.Const; +import teammates.e2e.pageobjects.InstructorCourseEnrollPage; +import teammates.e2e.pageobjects.InstructorCourseStudentDetailsEditPage; +import teammates.e2e.pageobjects.InstructorCourseStudentDetailsViewPage; +import teammates.e2e.pageobjects.InstructorStudentListPageSql; +import teammates.e2e.pageobjects.InstructorStudentRecordsPage; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.Instructor; +import teammates.storage.sqlentity.Student; + +/** + * SUT: {@link Const.WebPageURIs#INSTRUCTOR_STUDENT_LIST_PAGE}. + */ +public class InstructorStudentListPageE2ETest extends BaseE2ETestCase { + + @Override + protected void prepareTestData() { + testData = removeAndRestoreDataBundle( + loadSqlDataBundle("/InstructorStudentListPageE2ETestSql.json")); + } + + @Test + @Override + public void testAll() { + + ______TS("verify loaded data"); + + Instructor instructor = testData.instructors.get("instructorOfCourse1"); + String instructorId = instructor.getGoogleId(); + + AppUrl listPageUrl = createFrontendUrl(Const.WebPageURIs.INSTRUCTOR_STUDENT_LIST_PAGE); + InstructorStudentListPageSql listPage = loginToPage(listPageUrl, InstructorStudentListPageSql.class, instructorId); + + listPage.verifyAllCoursesHaveTabs(testData.courses.values()); + + Course course1 = testData.courses.get("course1"); + Course course2 = testData.courses.get("course2"); + Course course3 = testData.courses.get("course3"); + + // Expand all headers first + + listPage.clickCourseTabHeader(course1); + listPage.clickCourseTabHeader(course2); + listPage.clickCourseTabHeader(course3); + + Student[] studentsInCourse1 = {}; + + Student[] studentsInCourse3 = { + testData.students.get("Student1Course3"), + testData.students.get("Student2Course3"), + testData.students.get("Student3Course3"), + testData.students.get("Student4Course3"), + }; + + Map courseIdToStudents = new HashMap<>(); + courseIdToStudents.put(course1.getId(), studentsInCourse1); + courseIdToStudents.put(course3.getId(), studentsInCourse3); + + Map courseIdToCourse = new HashMap<>(); + courseIdToCourse.put(course1.getId(), course1); + courseIdToCourse.put(course3.getId(), course3); + + listPage.verifyStudentDetails(courseIdToCourse, courseIdToStudents); + listPage.verifyStudentDetailsNotViewable(course2); + + ______TS("link: enroll page"); + + InstructorCourseEnrollPage enrollPage = listPage.clickEnrollStudents(course3); + enrollPage.verifyIsCorrectPage(course3.getId()); + + listPage = getNewPageInstance(listPageUrl, InstructorStudentListPageSql.class); + listPage.clickCourseTabHeader(course3); + + ______TS("link: view student details page"); + + Student studentToView = testData.students.get("Student1Course3"); + String studentEmail = studentToView.getEmail(); + + InstructorCourseStudentDetailsViewPage studentDetailsViewPage = + listPage.clickViewStudent(course3, studentEmail); + studentDetailsViewPage.verifyIsCorrectPage(course3.getId(), studentEmail); + studentDetailsViewPage.closeCurrentWindowAndSwitchToParentWindow(); + + ______TS("link: edit student details page"); + + InstructorCourseStudentDetailsEditPage studentDetailsEditPage = + listPage.clickEditStudent(course3, studentEmail); + studentDetailsEditPage.verifyIsCorrectPage(course3.getId(), studentEmail); + studentDetailsEditPage.closeCurrentWindowAndSwitchToParentWindow(); + + ______TS("link: view all records page"); + + InstructorStudentRecordsPage studentRecordsPage = + listPage.clickViewAllRecords(course3, studentEmail); + studentRecordsPage.verifyIsCorrectPage(course3.getId(), studentToView.getName()); + studentRecordsPage.closeCurrentWindowAndSwitchToParentWindow(); + + ______TS("action: delete student"); + + Student studentToDelete = testData.students.get("Student3Course3"); + + listPage.deleteStudent(course3, studentToDelete.getEmail()); + + Student[] studentsAfterDelete = { + testData.students.get("Student1Course3"), + testData.students.get("Student2Course3"), + testData.students.get("Student4Course3"), + }; + + listPage.verifyStudentDetails(course3, studentsAfterDelete); + verifyAbsentInDatabase(studentToDelete); + + } + +} diff --git a/src/e2e/java/teammates/e2e/cases/sql/InstructorStudentRecordsPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/sql/InstructorStudentRecordsPageE2ETest.java new file mode 100644 index 000000000000..26fecac1b131 --- /dev/null +++ b/src/e2e/java/teammates/e2e/cases/sql/InstructorStudentRecordsPageE2ETest.java @@ -0,0 +1,44 @@ +package teammates.e2e.cases.sql; + +import org.testng.annotations.Test; + +import teammates.common.util.AppUrl; +import teammates.common.util.Const; +import teammates.e2e.pageobjects.InstructorStudentRecordsPage; +import teammates.storage.sqlentity.Instructor; +import teammates.storage.sqlentity.Student; + +/** + * SUT: {@link Const.WebPageURIs#INSTRUCTOR_STUDENT_RECORDS_PAGE}. + */ +public class InstructorStudentRecordsPageE2ETest extends BaseE2ETestCase { + + @Override + protected void prepareTestData() { + testData = removeAndRestoreDataBundle( + loadSqlDataBundle("/InstructorStudentRecordsPageE2ETestSql.json")); + } + + @Test + @Override + public void testAll() { + + ______TS("verify loaded data: student details"); + + Instructor instructor = testData.instructors.get("teammates.test.CS2104"); + Student student = testData.students.get("benny.c.tmms@ISR.CS2104"); + + String instructorId = instructor.getGoogleId(); + String courseId = instructor.getCourseId(); + String studentEmail = student.getEmail(); + + AppUrl recordsPageUrl = createFrontendUrl(Const.WebPageURIs.INSTRUCTOR_STUDENT_RECORDS_PAGE) + .withCourseId(courseId) + .withStudentEmail(studentEmail); + + InstructorStudentRecordsPage recordsPage = + loginToPage(recordsPageUrl, InstructorStudentRecordsPage.class, instructorId); + + recordsPage.verifyStudentDetails(student); + } +} diff --git a/src/e2e/java/teammates/e2e/cases/sql/RequestPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/sql/RequestPageE2ETest.java new file mode 100644 index 000000000000..4cb678a053f8 --- /dev/null +++ b/src/e2e/java/teammates/e2e/cases/sql/RequestPageE2ETest.java @@ -0,0 +1,52 @@ +package teammates.e2e.cases.sql; + +import org.testng.annotations.Test; + +import teammates.common.util.AppUrl; +import teammates.common.util.Const; +import teammates.common.util.EmailType; +import teammates.e2e.pageobjects.RequestPage; +import teammates.e2e.util.TestProperties; + +/** + * SUT: {@link Const.WebPageURIs#ACCOUNT_REQUEST_PAGE}. + */ +public class RequestPageE2ETest extends BaseE2ETestCase { + + @Override + protected void prepareTestData() { + // No test data needed + } + + @Test + @Override + protected void testAll() { + String name = "arf-test-name"; + String institution = "arf-test-institution"; + String country = "arf-test-country"; + String email = TestProperties.TEST_EMAIL; + String comments = "arf-test-comments"; + + AppUrl url = createFrontendUrl(Const.WebPageURIs.ACCOUNT_REQUEST_PAGE); + RequestPage requestPage = getNewPageInstance(url, RequestPage.class); + + ______TS("verify submission with comments"); + requestPage.clickAmInstructorButton(); + requestPage.fillForm(name, institution, country, email, comments); + requestPage.clickSubmitFormButton(); + requestPage.verifySubmittedInfo(name, institution, country, email, comments); + + String expectedEmailSubject = EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT.toString(); + verifyEmailSent(email, expectedEmailSubject); + + ______TS("verify submission without comments"); + requestPage = getNewPageInstance(url, RequestPage.class); + requestPage.clickAmInstructorButton(); + requestPage.fillForm(name, institution, country, email, ""); + requestPage.clickSubmitFormButton(); + requestPage.verifySubmittedInfo(name, institution, country, email, ""); + + expectedEmailSubject = EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT.toString(); + verifyEmailSent(email, expectedEmailSubject); + } +} diff --git a/src/e2e/java/teammates/e2e/cases/sql/StudentCourseDetailsPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/sql/StudentCourseDetailsPageE2ETest.java new file mode 100644 index 000000000000..0e3cbc2b065b --- /dev/null +++ b/src/e2e/java/teammates/e2e/cases/sql/StudentCourseDetailsPageE2ETest.java @@ -0,0 +1,43 @@ +package teammates.e2e.cases.sql; + +import org.testng.annotations.Test; + +import teammates.common.util.AppUrl; +import teammates.common.util.Const; +import teammates.e2e.pageobjects.StudentCourseDetailsPage; +import teammates.storage.sqlentity.Instructor; +import teammates.storage.sqlentity.Student; + +/** + * SUT: {@link Const.WebPageURIs#STUDENT_COURSE_DETAILS_PAGE}. + */ +public class StudentCourseDetailsPageE2ETest extends BaseE2ETestCase { + + @Override + protected void prepareTestData() { + testData = removeAndRestoreDataBundle( + loadSqlDataBundle("/StudentCourseDetailsPageE2ETestSql.json")); + } + + @Test + @Override + public void testAll() { + + AppUrl url = createFrontendUrl(Const.WebPageURIs.STUDENT_COURSE_DETAILS_PAGE) + .withCourseId("tm.e2e.SCDet.CS2104"); + StudentCourseDetailsPage detailsPage = loginToPage(url, StudentCourseDetailsPage.class, + testData.accounts.get("SCDet.alice").getGoogleId()); + + ______TS("verify loaded data"); + Instructor[] instructors = { testData.instructors.get("SCDet.instr"), + testData.instructors.get("SCDet.instr2") }; + Student[] teammates = { testData.students.get("SCDet.benny"), + testData.students.get("SCDet.charlie") }; + + detailsPage.verifyCourseDetails(testData.courses.get("SCDet.CS2104")); + detailsPage.verifyInstructorsDetails(instructors); + detailsPage.verifyStudentDetails(testData.students.get("SCDet.alice")); + detailsPage.sortTeammatesByName(); + detailsPage.verifyTeammatesDetails(teammates); + } +} diff --git a/src/e2e/java/teammates/e2e/cases/sql/StudentCourseJoinConfirmationPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/sql/StudentCourseJoinConfirmationPageE2ETest.java new file mode 100644 index 000000000000..807c7caf2bdb --- /dev/null +++ b/src/e2e/java/teammates/e2e/cases/sql/StudentCourseJoinConfirmationPageE2ETest.java @@ -0,0 +1,56 @@ +package teammates.e2e.cases.sql; + +import org.testng.annotations.Test; + +import teammates.common.util.AppUrl; +import teammates.common.util.Const; +import teammates.e2e.pageobjects.CourseJoinConfirmationPage; +import teammates.e2e.pageobjects.StudentHomePage; +import teammates.storage.sqlentity.Student; + +/** + * SUT: {@link Const.WebPageURIs#JOIN_PAGE}. + */ +public class StudentCourseJoinConfirmationPageE2ETest extends BaseE2ETestCase { + private Student newStudent; + private String newStudentGoogleId; + + @Override + protected void prepareTestData() { + testData = removeAndRestoreDataBundle(loadSqlDataBundle("/StudentCourseJoinConfirmationPageE2ETestSql.json")); + + newStudent = testData.students.get("alice.tmms@SCJoinConf.CS2104"); + newStudentGoogleId = testData.accounts.get("alice.tmms").getGoogleId(); + } + + @Test + @Override + public void testAll() { + ______TS("Click join link: invalid key"); + String courseId = testData.courses.get("SCJoinConf.CS2104").getId(); + String invalidKey = "invalidKey"; + AppUrl joinLink = createFrontendUrl(Const.WebPageURIs.JOIN_PAGE) + .withRegistrationKey(invalidKey) + .withCourseId(courseId) + .withEntityType(Const.EntityType.STUDENT); + CourseJoinConfirmationPage confirmationPage = loginToPage( + joinLink, CourseJoinConfirmationPage.class, newStudentGoogleId); + + confirmationPage.verifyDisplayedMessage("The course join link is invalid. You may have " + + "entered the URL incorrectly or the URL may correspond to a/an student that does not exist."); + + ______TS("Click join link: valid key"); + joinLink = createFrontendUrl(Const.WebPageURIs.JOIN_PAGE) + .withRegistrationKey(newStudent.getRegKey()) + .withCourseId(courseId) + .withEntityType(Const.EntityType.STUDENT); + confirmationPage = getNewPageInstance(joinLink, CourseJoinConfirmationPage.class); + + confirmationPage.verifyJoiningUser(newStudentGoogleId); + confirmationPage.confirmJoinCourse(StudentHomePage.class); + + ______TS("Already joined, no confirmation page"); + + getNewPageInstance(joinLink, StudentHomePage.class); + } +} diff --git a/src/e2e/java/teammates/e2e/pageobjects/AdminAccountsPage.java b/src/e2e/java/teammates/e2e/pageobjects/AdminAccountsPage.java index aec053d21b2b..3848cb334177 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/AdminAccountsPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/AdminAccountsPage.java @@ -1,7 +1,7 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import java.util.List; diff --git a/src/e2e/java/teammates/e2e/pageobjects/AdminHomePage.java b/src/e2e/java/teammates/e2e/pageobjects/AdminHomePage.java index 7e40f8ebef4e..9316f0420779 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/AdminHomePage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/AdminHomePage.java @@ -1,5 +1,7 @@ package teammates.e2e.pageobjects; +import static org.junit.Assert.assertNotNull; + import java.util.List; import org.openqa.selenium.By; @@ -12,6 +14,9 @@ * Represents the admin home page of the website. */ public class AdminHomePage extends AppPage { + private static final int ACCOUNT_REQUEST_COL_NAME = 1; + private static final int ACCOUNT_REQUEST_COL_EMAIL = 2; + private static final int ACCOUNT_REQUEST_COL_INSTITUTE = 4; @FindBy(id = "instructor-details-single-line") private WebElement detailsSingleLineTextBox; @@ -95,4 +100,29 @@ public void clickResetAccountRequestLink() { List okButtons = browser.driver.findElements(By.className("modal-btn-ok")); clickDismissModalButtonAndWaitForModalHidden(okButtons.get(1)); // Second modal is confirmation modal } + + public String removeSpanFromText(String text) { + return text.replace("", "").replace("", ""); + } + + public WebElement getAccountRequestRow(String name, String email, String institute) { + List rows = browser.driver.findElements(By.cssSelector("tm-account-request-table tbody tr")); + for (WebElement row : rows) { + List columns = row.findElements(By.tagName("td")); + if (removeSpanFromText(columns.get(ACCOUNT_REQUEST_COL_NAME - 1) + .getAttribute("innerHTML")).contains(name) + && removeSpanFromText(columns.get(ACCOUNT_REQUEST_COL_EMAIL - 1) + .getAttribute("innerHTML")).contains(email) + && removeSpanFromText(columns.get(ACCOUNT_REQUEST_COL_INSTITUTE - 1) + .getAttribute("innerHTML")).contains(institute)) { + return row; + } + } + return null; + } + + public void verifyInstructorInAccountRequestTable(String name, String email, String institute) { + WebElement row = getAccountRequestRow(name, email, institute); + assertNotNull(row); + } } diff --git a/src/e2e/java/teammates/e2e/pageobjects/AdminNotificationsPage.java b/src/e2e/java/teammates/e2e/pageobjects/AdminNotificationsPage.java index ef01c2b70fa1..b483a53753a8 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/AdminNotificationsPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/AdminNotificationsPage.java @@ -119,7 +119,7 @@ public String getFirstRowNotificationId() { public void sortNotificationsTableByDescendingCreateTime() { WebElement creationTimeHeader = notificationsTable.findElements(By.tagName("th")).get(5); - if (creationTimeHeader.findElements(By.className("fa-sort-down")).size() == 0) { + if (creationTimeHeader.findElements(By.className("fa-sort-down")).isEmpty()) { click(creationTimeHeader); } } diff --git a/src/e2e/java/teammates/e2e/pageobjects/AdminSearchPage.java b/src/e2e/java/teammates/e2e/pageobjects/AdminSearchPage.java index 706e9ab5a207..441b3e1512e8 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/AdminSearchPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/AdminSearchPage.java @@ -1,8 +1,8 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import java.util.List; @@ -18,6 +18,9 @@ import teammates.common.util.Const; import teammates.common.util.StringHelper; import teammates.storage.sqlentity.AccountRequest; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.Instructor; +import teammates.storage.sqlentity.Student; /** * Represents the admin home page of the website. @@ -38,9 +41,9 @@ public class AdminSearchPage extends AppPage { private static final int ACCOUNT_REQUEST_COL_NAME = 1; private static final int ACCOUNT_REQUEST_COL_EMAIL = 2; - private static final int ACCOUNT_REQUEST_COL_INSTITUTE = 3; - private static final int ACCOUNT_REQUEST_COL_CREATED_AT = 4; - private static final int ACCOUNT_REQUEST_COL_REGISTERED_AT = 5; + private static final int ACCOUNT_REQUEST_COL_INSTITUTE = 4; + private static final int ACCOUNT_REQUEST_COL_CREATED_AT = 5; + private static final int ACCOUNT_REQUEST_COL_REGISTERED_AT = 6; private static final String EXPANDED_ROWS_HEADER_EMAIL = "Email"; private static final String EXPANDED_ROWS_HEADER_COURSE_JOIN_LINK = "Course Join Link"; @@ -93,6 +96,14 @@ public void clickSearchButton() { waitForPageToLoad(); } + public void regenerateStudentKey(Student student) { + WebElement studentRow = getStudentRow(student); + studentRow.findElement(By.xpath("//button[text()='Regenerate key']")).click(); + + waitForConfirmationModalAndClickOk(); + waitForPageToLoad(true); + } + public void regenerateStudentKey(StudentAttributes student) { WebElement studentRow = getStudentRow(student); studentRow.findElement(By.xpath("//button[text()='Regenerate key']")).click(); @@ -101,6 +112,30 @@ public void regenerateStudentKey(StudentAttributes student) { waitForPageToLoad(true); } + public void verifyRegenerateStudentKey(Student student, String originalJoinLink) { + verifyStatusMessage("Student's key for this course has been successfully regenerated," + + " and the email has been sent."); + + String regeneratedJoinLink = getStudentJoinLink(student); + assertNotEquals(regeneratedJoinLink, originalJoinLink); + } + + public void verifyRegenerateStudentKey(StudentAttributes student, String originalJoinLink) { + verifyStatusMessage("Student's key for this course has been successfully regenerated," + + " and the email has been sent."); + + String regeneratedJoinLink = getStudentJoinLink(student); + assertNotEquals(regeneratedJoinLink, originalJoinLink); + } + + public void regenerateInstructorKey(Instructor instructor) { + WebElement instructorRow = getInstructorRow(instructor); + instructorRow.findElement(By.xpath("//button[text()='Regenerate key']")).click(); + + waitForConfirmationModalAndClickOk(); + waitForPageToLoad(true); + } + public void regenerateInstructorKey(InstructorAttributes instructor) { WebElement instructorRow = getInstructorRow(instructor); instructorRow.findElement(By.xpath("//button[text()='Regenerate key']")).click(); @@ -143,6 +178,25 @@ public String removeSpanFromText(String text) { return text.replace("", "").replace("", ""); } + public WebElement getStudentRow(Student student) { + String details = String.format("%s [%s] (%s)", student.getCourse().getId(), + student.getSection() == null + ? Const.DEFAULT_SECTION + : student.getSection().getName(), student.getTeam().getName()); + WebElement table = browser.driver.findElement(By.id("search-table-student")); + List rows = table.findElements(By.tagName("tr")); + for (WebElement row : rows) { + List columns = row.findElements(By.tagName("td")); + if (!columns.isEmpty() && removeSpanFromText(columns.get(STUDENT_COL_DETAILS - 1) + .getAttribute("innerHTML")).contains(details) + && removeSpanFromText(columns.get(STUDENT_COL_NAME - 1) + .getAttribute("innerHTML")).contains(student.getName())) { + return row; + } + } + return null; + } + public WebElement getStudentRow(StudentAttributes student) { String details = String.format("%s [%s] (%s)", student.getCourse(), student.getSection() == null ? Const.DEFAULT_SECTION : student.getSection(), student.getTeam()); @@ -195,11 +249,25 @@ public String getStudentJoinLink(WebElement studentRow) { return getExpandedRowInputValue(studentRow, EXPANDED_ROWS_HEADER_COURSE_JOIN_LINK); } + public String getStudentJoinLink(Student student) { + WebElement studentRow = getStudentRow(student); + return getStudentJoinLink(studentRow); + } + public String getStudentJoinLink(StudentAttributes student) { WebElement studentRow = getStudentRow(student); return getStudentJoinLink(studentRow); } + public void resetStudentGoogleId(Student student) { + WebElement studentRow = getStudentRow(student); + WebElement link = studentRow.findElement(By.linkText(LINK_TEXT_RESET_GOOGLE_ID)); + link.click(); + + waitForConfirmationModalAndClickOk(); + waitForElementStaleness(link); + } + public void resetStudentGoogleId(StudentAttributes student) { WebElement studentRow = getStudentRow(student); WebElement link = studentRow.findElement(By.linkText(LINK_TEXT_RESET_GOOGLE_ID)); @@ -209,6 +277,21 @@ public void resetStudentGoogleId(StudentAttributes student) { waitForElementStaleness(link); } + public WebElement getInstructorRow(Instructor instructor) { + WebElement table = browser.driver.findElement(By.id("search-table-instructor")); + List rows = table.findElements(By.tagName("tr")); + for (WebElement row : rows) { + List columns = row.findElements(By.tagName("td")); + if (columns.size() >= 3 && (removeSpanFromText(columns.get(2) + .getAttribute("innerHTML")).contains(instructor.getGoogleId()) + || removeSpanFromText(columns.get(1) + .getAttribute("innerHTML")).contains(instructor.getName()))) { + return row; + } + } + return null; + } + public WebElement getInstructorRow(InstructorAttributes instructor) { String courseId = instructor.getCourseId(); List rows = browser.driver.findElements(By.cssSelector("#search-table-instructor tbody tr")); @@ -256,11 +339,25 @@ public String getInstructorJoinLink(WebElement instructorRow) { return getExpandedRowInputValue(instructorRow, EXPANDED_ROWS_HEADER_COURSE_JOIN_LINK); } + public String getInstructorJoinLink(Instructor instructor) { + WebElement instructorRow = getInstructorRow(instructor); + return getInstructorJoinLink(instructorRow); + } + public String getInstructorJoinLink(InstructorAttributes instructor) { WebElement instructorRow = getInstructorRow(instructor); return getInstructorJoinLink(instructorRow); } + public void resetInstructorGoogleId(Instructor instructor) { + WebElement instructorRow = getInstructorRow(instructor); + WebElement link = instructorRow.findElement(By.linkText(LINK_TEXT_RESET_GOOGLE_ID)); + link.click(); + + waitForConfirmationModalAndClickOk(); + waitForElementStaleness(link); + } + public void resetInstructorGoogleId(InstructorAttributes instructor) { WebElement instructorRow = getInstructorRow(instructor); WebElement link = instructorRow.findElement(By.linkText(LINK_TEXT_RESET_GOOGLE_ID)); @@ -273,7 +370,7 @@ public void resetInstructorGoogleId(InstructorAttributes instructor) { public WebElement getAccountRequestRow(AccountRequestAttributes accountRequest) { String email = accountRequest.getEmail(); String institute = accountRequest.getInstitute(); - List rows = browser.driver.findElements(By.cssSelector("#search-table-account-request tbody tr")); + List rows = browser.driver.findElements(By.cssSelector("tm-account-request-table tbody tr")); for (WebElement row : rows) { List columns = row.findElements(By.tagName("td")); if (removeSpanFromText(columns.get(ACCOUNT_REQUEST_COL_EMAIL - 1) @@ -289,7 +386,7 @@ && removeSpanFromText(columns.get(ACCOUNT_REQUEST_COL_INSTITUTE - 1) public WebElement getAccountRequestRow(AccountRequest accountRequest) { String email = accountRequest.getEmail(); String institute = accountRequest.getInstitute(); - List rows = browser.driver.findElements(By.cssSelector("#search-table-account-request tbody tr")); + List rows = browser.driver.findElements(By.cssSelector("tm-account-request-table tbody tr")); for (WebElement row : rows) { List columns = row.findElements(By.tagName("td")); if (removeSpanFromText(columns.get(ACCOUNT_REQUEST_COL_EMAIL - 1) @@ -342,6 +439,111 @@ public void clickDeleteAccountRequestButton(AccountRequest accountRequest) { waitForPageToLoad(); } + public void clickApproveAccountRequestButton(AccountRequest accountRequest) { + WebElement accountRequestRow = getAccountRequestRow(accountRequest); + waitForElementPresence(By.cssSelector("[id^='approve-account-request-']")); + WebElement approveButton = accountRequestRow.findElement(By.cssSelector("[id^='approve-account-request-']")); + waitForElementToBeClickable(approveButton); + approveButton.click(); + waitForPageToLoad(); + } + + public void clickRejectAccountRequestButton(AccountRequest accountRequest) { + WebElement accountRequestRow = getAccountRequestRow(accountRequest); + WebElement rejectButton = accountRequestRow.findElement(By.cssSelector("[id^='reject-account-request-']")); + rejectButton.click(); + waitForPageToLoad(); + WebElement rejectWithoutReasonButton = browser.driver.findElement(By.cssSelector("[id^='reject-request-']")); + rejectWithoutReasonButton.click(); + waitForPageToLoad(); + } + + public void clickRejectAccountRequestWithReasonButton(AccountRequest accountRequest) { + WebElement accountRequestRow = getAccountRequestRow(accountRequest); + WebElement rejectButton = accountRequestRow.findElement(By.cssSelector("[id^='reject-account-request-']")); + rejectButton.click(); + waitForPageToLoad(); + WebElement rejectWithReasonButton = browser.driver.findElement(By.cssSelector("[id^='reject-request-with-reason']")); + waitForElementToBeClickable(rejectWithReasonButton); + rejectWithReasonButton.click(); + waitForPageToLoad(); + waitForElementPresence(By.cssSelector("tm-reject-with-reason-modal")); + } + + public void fillInRejectionModalTitle(String title) { + WebElement rejectionModal = browser.driver.findElement(By.cssSelector("tm-reject-with-reason-modal")); + WebElement titleInput = rejectionModal.findElement(By.cssSelector("[id^='rejection-reason-title']")); + titleInput.clear(); + titleInput.sendKeys(title); + } + + public void fillInRejectionModalBody(String body) { + WebElement rejectionModal = browser.driver.findElement(By.cssSelector("tm-reject-with-reason-modal")); + WebElement bodyInput = rejectionModal.findElement(By.cssSelector("tm-rich-text-editor")); + clearRichTextEditor(bodyInput); + writeToRichTextEditor(bodyInput, body); + } + + public void clickConfirmRejectAccountRequest() { + WebElement rejectionModal = browser.driver.findElement(By.cssSelector("tm-reject-with-reason-modal")); + WebElement clickReject = rejectionModal.findElement(By.cssSelector("[id^='btn-confirm-reject-request']")); + clickReject.click(); + waitForPageToLoad(); + } + + public void closeRejectionModal() { + WebElement rejectionModal = browser.driver.findElement(By.cssSelector("tm-reject-with-reason-modal")); + WebElement clickCancel = rejectionModal.findElement(By.cssSelector("[id^='btn-cancel-reject-request']")); + clickCancel.click(); + waitForPageToLoad(); + } + + public void clickEditAccountRequestButton(AccountRequest accountRequest) { + WebElement accountRequestRow = getAccountRequestRow(accountRequest); + WebElement editButton = accountRequestRow.findElement(By.cssSelector("[id^='edit-account-request-']")); + editButton.click(); + waitForElementPresence(By.cssSelector("tm-edit-request-modal")); + } + + public void fillInEditModalFields(String name, String email, String institute, String comments) { + waitForElementPresence(By.cssSelector("tm-edit-request-modal")); + + WebElement editModal = browser.driver.findElement(By.cssSelector("tm-edit-request-modal")); + WebElement nameInput = editModal.findElement(By.cssSelector("[id^='request-name']")); + nameInput.clear(); + nameInput.sendKeys(name); + + WebElement emailInput = editModal.findElement(By.cssSelector("[id^='request-email']")); + emailInput.clear(); + emailInput.sendKeys(email); + + WebElement instituteInput = editModal.findElement(By.cssSelector("[id^='request-institution']")); + instituteInput.clear(); + instituteInput.sendKeys(institute); + + WebElement commentsInput = editModal.findElement(By.cssSelector("[id^='request-comments']")); + commentsInput.clear(); + commentsInput.sendKeys(comments); + } + + public void clickSaveEditAccountRequestButton() { + WebElement editModal = browser.driver.findElement(By.cssSelector("tm-edit-request-modal")); + WebElement saveButton = editModal.findElement(By.cssSelector("[id^='btn-confirm-edit-request']")); + saveButton.click(); + waitForPageToLoad(); + } + + public void clickViewAccountRequestAndVerifyCommentsButton(AccountRequest accountRequest, String comments) { + WebElement accountRequestRow = getAccountRequestRow(accountRequest); + WebElement viewCommentsButton = accountRequestRow.findElement(By.cssSelector("[id^='view-account-request-']")); + viewCommentsButton.click(); + waitForElementVisibility(By.className("modal-btn-ok")); + WebElement modal = browser.driver.findElement(By.className("modal-body")); + String actualComments = modal.findElement(By.tagName("div")).getText(); + assertEquals("Comment: " + comments, actualComments); + waitForConfirmationModalAndClickOk(); + } + public void clickResetAccountRequestButton(AccountRequestAttributes accountRequest) { WebElement accountRequestRow = getAccountRequestRow(accountRequest); WebElement deleteButton = accountRequestRow.findElement(By.cssSelector("[id^='reset-account-request-']")); @@ -386,6 +588,32 @@ private String getExpandedRowInputValue(WebElement row, String rowHeader) { } } + public void verifyStudentRowContent(Student student, Course course, + String expectedDetails, String expectedManageAccountLink, + String expectedHomePageLink) { + WebElement studentRow = getStudentRow(student); + String actualDetails = getStudentDetails(studentRow); + String actualName = getStudentName(studentRow); + String actualGoogleId = getStudentGoogleId(studentRow); + String actualHomepageLink = getStudentHomeLink(studentRow); + String actualInstitute = getStudentInstitute(studentRow); + String actualComment = getStudentComments(studentRow); + String actualManageAccountLink = getStudentManageAccountLink(studentRow); + + String expectedName = student.getName(); + String expectedGoogleId = StringHelper.convertToEmptyStringIfNull(student.getGoogleId()); + String expectedInstitute = StringHelper.convertToEmptyStringIfNull(course.getInstitute()); + String expectedComment = StringHelper.convertToEmptyStringIfNull(student.getComments()); + + assertEquals(expectedDetails, actualDetails); + assertEquals(expectedName, actualName); + assertEquals(expectedGoogleId, actualGoogleId); + assertEquals(expectedInstitute, actualInstitute); + assertEquals(expectedComment, actualComment); + assertEquals(expectedManageAccountLink, actualManageAccountLink); + assertEquals(expectedHomePageLink, actualHomepageLink); + } + public void verifyStudentRowContent(StudentAttributes student, CourseAttributes course, String expectedDetails, String expectedManageAccountLink, String expectedHomePageLink) { @@ -412,6 +640,35 @@ public void verifyStudentRowContent(StudentAttributes student, CourseAttributes assertEquals(expectedHomePageLink, actualHomepageLink); } + public void verifyStudentRowContentAfterReset(Student student, Course course) { + WebElement studentRow = getStudentRow(student); + String actualName = getStudentName(studentRow); + String actualInstitute = getStudentInstitute(studentRow); + String actualComment = getStudentComments(studentRow); + + String expectedName = student.getName(); + String expectedInstitute = StringHelper.convertToEmptyStringIfNull(course.getInstitute()); + String expectedComment = StringHelper.convertToEmptyStringIfNull(student.getComments()); + + assertEquals(expectedName, actualName); + assertEquals(expectedInstitute, actualInstitute); + assertEquals(expectedComment, actualComment); + } + + public void verifyStudentExpandedLinks(Student student, int expectedNumExpandedRows) { + clickExpandStudentLinks(); + WebElement studentRow = getStudentRow(student); + String actualEmail = getStudentEmail(studentRow); + String actualJoinLink = getStudentJoinLink(studentRow); + int actualNumExpandedRows = getNumExpandedRows(studentRow); + + String expectedEmail = student.getEmail(); + + assertEquals(expectedEmail, actualEmail); + assertNotEquals("", actualJoinLink); + assertEquals(expectedNumExpandedRows, actualNumExpandedRows); + } + public void verifyStudentExpandedLinks(StudentAttributes student, int expectedNumExpandedRows) { clickExpandStudentLinks(); WebElement studentRow = getStudentRow(student); @@ -426,6 +683,29 @@ public void verifyStudentExpandedLinks(StudentAttributes student, int expectedNu assertEquals(expectedNumExpandedRows, actualNumExpandedRows); } + public void verifyInstructorRowContent(Instructor instructor, Course course, + String expectedManageAccountLink, String expectedHomePageLink) { + WebElement instructorRow = getInstructorRow(instructor); + String actualCourseId = getInstructorCourseId(instructorRow); + String actualName = getInstructorName(instructorRow); + String actualGoogleId = getInstructorGoogleId(instructorRow); + String actualHomePageLink = getInstructorHomePageLink(instructorRow); + String actualInstitute = getInstructorInstitute(instructorRow); + String actualManageAccountLink = getInstructorManageAccountLink(instructorRow); + + String expectedCourseId = instructor.getCourseId(); + String expectedName = instructor.getName(); + String expectedGoogleId = StringHelper.convertToEmptyStringIfNull(instructor.getGoogleId()); + String expectedInstitute = StringHelper.convertToEmptyStringIfNull(course.getInstitute()); + + assertEquals(expectedCourseId, actualCourseId); + assertEquals(expectedName, actualName); + assertEquals(expectedGoogleId, actualGoogleId); + assertEquals(expectedHomePageLink, actualHomePageLink); + assertEquals(expectedInstitute, actualInstitute); + assertEquals(expectedManageAccountLink, actualManageAccountLink); + } + public void verifyInstructorRowContent(InstructorAttributes instructor, CourseAttributes course, String expectedManageAccountLink, String expectedHomePageLink) { WebElement instructorRow = getInstructorRow(instructor); @@ -449,6 +729,33 @@ public void verifyInstructorRowContent(InstructorAttributes instructor, CourseAt assertEquals(expectedManageAccountLink, actualManageAccountLink); } + public void verifyInstructorRowContentAfterReset(Instructor instructor, Course course) { + WebElement instructorRow = getInstructorRow(instructor); + String actualCourseId = getInstructorCourseId(instructorRow); + String actualName = getInstructorName(instructorRow); + String actualInstitute = getInstructorInstitute(instructorRow); + + String expectedCourseId = instructor.getCourseId(); + String expectedName = instructor.getName(); + String expectedInstitute = StringHelper.convertToEmptyStringIfNull(course.getInstitute()); + + assertEquals(expectedCourseId, actualCourseId); + assertEquals(expectedName, actualName); + assertEquals(expectedInstitute, actualInstitute); + } + + public void verifyInstructorExpandedLinks(Instructor instructor) { + clickExpandInstructorLinks(); + WebElement instructorRow = getInstructorRow(instructor); + String actualEmail = getInstructorEmail(instructorRow); + String actualJoinLink = getInstructorJoinLink(instructorRow); + + String expectedEmail = instructor.getEmail(); + + assertEquals(expectedEmail, actualEmail); + assertNotEquals("", actualJoinLink); + } + public void verifyInstructorExpandedLinks(InstructorAttributes instructor) { clickExpandInstructorLinks(); WebElement instructorRow = getInstructorRow(instructor); @@ -515,6 +822,43 @@ public void verifyAccountRequestExpandedLinks(AccountRequest accountRequest) { assertFalse(actualRegistrationLink.isBlank()); } + public void verifyLinkExpansionButtons(Student student, + Instructor instructor, AccountRequest accountRequest) { + WebElement studentRow = getStudentRow(student); + WebElement instructorRow = getInstructorRow(instructor); + WebElement accountRequestRow = getAccountRequestRow(accountRequest); + + clickExpandStudentLinks(); + clickExpandInstructorLinks(); + clickExpandAccountRequestLinks(); + int numExpandedStudentRows = getNumExpandedRows(studentRow); + int numExpandedInstructorRows = getNumExpandedRows(instructorRow); + int numExpandedAccountRequestRows = getNumExpandedRows(accountRequestRow); + assertNotEquals(numExpandedStudentRows, 0); + assertNotEquals(numExpandedInstructorRows, 0); + assertNotEquals(numExpandedAccountRequestRows, 0); + + clickCollapseInstructorLinks(); + numExpandedStudentRows = getNumExpandedRows(studentRow); + numExpandedInstructorRows = getNumExpandedRows(instructorRow); + numExpandedAccountRequestRows = getNumExpandedRows(accountRequestRow); + assertNotEquals(numExpandedStudentRows, 0); + assertEquals(numExpandedInstructorRows, 0); + assertNotEquals(numExpandedAccountRequestRows, 0); + + clickExpandInstructorLinks(); + clickCollapseStudentLinks(); + clickCollapseAccountRequestLinks(); + waitUntilAnimationFinish(); + + numExpandedStudentRows = getNumExpandedRows(studentRow); + numExpandedInstructorRows = getNumExpandedRows(instructorRow); + numExpandedAccountRequestRows = getNumExpandedRows(accountRequestRow); + assertEquals(numExpandedStudentRows, 0); + assertNotEquals(numExpandedInstructorRows, 0); + assertEquals(numExpandedAccountRequestRows, 0); + } + public void verifyLinkExpansionButtons(StudentAttributes student, InstructorAttributes instructor, AccountRequestAttributes accountRequest) { WebElement studentRow = getStudentRow(student); @@ -589,11 +933,11 @@ public void verifyLinkExpansionButtons(StudentAttributes student, assertEquals(numExpandedAccountRequestRows, 0); } - public void verifyRegenerateStudentKey(StudentAttributes student, String originalJoinLink) { - verifyStatusMessage("Student's key for this course has been successfully regenerated," + public void verifyRegenerateInstructorKey(Instructor instructor, String originalJoinLink) { + verifyStatusMessage("Instructor's key for this course has been successfully regenerated," + " and the email has been sent."); - String regeneratedJoinLink = getStudentJoinLink(student); + String regeneratedJoinLink = getInstructorJoinLink(instructor); assertNotEquals(regeneratedJoinLink, originalJoinLink); } @@ -604,5 +948,4 @@ public void verifyRegenerateInstructorKey(InstructorAttributes instructor, Strin String regeneratedJoinLink = getInstructorJoinLink(instructor); assertNotEquals(regeneratedJoinLink, originalJoinLink); } - } diff --git a/src/e2e/java/teammates/e2e/pageobjects/AdminSessionsPage.java b/src/e2e/java/teammates/e2e/pageobjects/AdminSessionsPage.java index 97f1d08b1314..82bb8b5f09c1 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/AdminSessionsPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/AdminSessionsPage.java @@ -1,6 +1,6 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.Instant; import java.util.List; diff --git a/src/e2e/java/teammates/e2e/pageobjects/AppPage.java b/src/e2e/java/teammates/e2e/pageobjects/AppPage.java index c8d5911e57f9..52ae7cfdf601 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/AppPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/AppPage.java @@ -1,10 +1,10 @@ package teammates.e2e.pageobjects; import static com.google.common.base.Preconditions.checkNotNull; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; import java.io.IOException; @@ -395,6 +395,13 @@ protected void writeToRichTextEditor(WebElement editor, String text) { ((JavascriptExecutor) browser.driver).executeAsyncScript(WRITE_TO_TINYMCE_SCRIPT, id, text); } + /** + * Clear existing text in the editor. + */ + protected void clearRichTextEditor(WebElement editor) { + writeToRichTextEditor(editor, ""); + } + /** * Select the option, if it is not already selected. * No action taken if it is already selected. @@ -542,7 +549,7 @@ public AppPage clickAndConfirm(WebElement elementToClick) { * Returns True if there is a corresponding element for the given locator. */ public boolean isElementPresent(By by) { - return browser.driver.findElements(by).size() != 0; + return !browser.driver.findElements(by).isEmpty(); } /** diff --git a/src/e2e/java/teammates/e2e/pageobjects/Browser.java b/src/e2e/java/teammates/e2e/pageobjects/Browser.java index b23a39213de2..762f93070b47 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/Browser.java +++ b/src/e2e/java/teammates/e2e/pageobjects/Browser.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.time.Duration; import java.util.ArrayDeque; +import java.util.Deque; import java.util.HashMap; import java.util.Map; @@ -47,7 +48,7 @@ public class Browser { /** * Keeps track of multiple windows opened by the {@link WebDriver}. */ - private final ArrayDeque windowHandles = new ArrayDeque<>(); + private final Deque windowHandles = new ArrayDeque<>(); public Browser() { this.driver = createWebDriver(); diff --git a/src/e2e/java/teammates/e2e/pageobjects/CourseJoinConfirmationPage.java b/src/e2e/java/teammates/e2e/pageobjects/CourseJoinConfirmationPage.java index 5f5ef6785abf..98defd18e564 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/CourseJoinConfirmationPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/CourseJoinConfirmationPage.java @@ -1,6 +1,6 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; diff --git a/src/e2e/java/teammates/e2e/pageobjects/ErrorReportingModal.java b/src/e2e/java/teammates/e2e/pageobjects/ErrorReportingModal.java index 0dbd67eba735..87bfcee3b8da 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/ErrorReportingModal.java +++ b/src/e2e/java/teammates/e2e/pageobjects/ErrorReportingModal.java @@ -1,6 +1,6 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import org.openqa.selenium.By; diff --git a/src/e2e/java/teammates/e2e/pageobjects/FeedbackResultsPage.java b/src/e2e/java/teammates/e2e/pageobjects/FeedbackResultsPage.java index 07c75951cb5b..088e044c34c7 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/FeedbackResultsPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/FeedbackResultsPage.java @@ -1,8 +1,8 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.time.Instant; import java.util.ArrayList; @@ -296,7 +296,7 @@ private boolean isRubricResponseEqual(WebElement responseField, FeedbackResponse WebElement rubricTableBody = responseField.findElement(By.className("rubric-answers")); WebElement rubricRow = rubricTableBody.findElements(By.cssSelector("tr")).get(i); WebElement rubricCell = rubricRow.findElements(By.tagName("td")).get(answers.get(i) + 1); - if (rubricCell.findElements(By.className("fa-check")).size() == 0) { + if (rubricCell.findElements(By.className("fa-check")).isEmpty()) { return false; } } @@ -574,7 +574,7 @@ private WebElement getRubricStatistics(int questionNum) { } private boolean isCommentByResponseGiver(WebElement commentField) { - return commentField.findElements(By.className("by-response-giver")).size() > 0; + return !commentField.findElements(By.className("by-response-giver")).isEmpty(); } private String getCommentGiver(WebElement commentField) { diff --git a/src/e2e/java/teammates/e2e/pageobjects/FeedbackSubmitPage.java b/src/e2e/java/teammates/e2e/pageobjects/FeedbackSubmitPage.java index e32bbcaaa96e..f7262e63ffb5 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/FeedbackSubmitPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/FeedbackSubmitPage.java @@ -1,9 +1,9 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.time.Instant; import java.util.Collections; @@ -392,6 +392,12 @@ public void fillNumScaleResponse(int qnNumber, String recipient, FeedbackRespons fillTextBox(getNumScaleInput(qnNumber, recipient), Double.toString(responseDetails.getAnswer())); } + public void fillNumScaleResponse(int qnNumber, String recipient, FeedbackResponse response) { + FeedbackNumericalScaleResponseDetails responseDetails = + (FeedbackNumericalScaleResponseDetails) response.getFeedbackResponseDetailsCopy(); + fillTextBox(getNumScaleInput(qnNumber, recipient), Double.toString(responseDetails.getAnswer())); + } + public void verifyNumScaleResponse(int qnNumber, String recipient, FeedbackResponseAttributes response) { FeedbackNumericalScaleResponseDetails responseDetails = (FeedbackNumericalScaleResponseDetails) response.getResponseDetailsCopy(); @@ -399,6 +405,13 @@ public void verifyNumScaleResponse(int qnNumber, String recipient, FeedbackRespo getDoubleString(responseDetails.getAnswer())); } + public void verifyNumScaleResponse(int qnNumber, String recipient, FeedbackResponse response) { + FeedbackNumericalScaleResponseDetails responseDetails = + (FeedbackNumericalScaleResponseDetails) response.getFeedbackResponseDetailsCopy(); + assertEquals(getNumScaleInput(qnNumber, recipient).getAttribute("value"), + getDoubleString(responseDetails.getAnswer())); + } + public void verifyConstSumQuestion(int qnNumber, String recipient, FeedbackConstantSumQuestionDetails questionDetails) { if (!questionDetails.isDistributeToRecipients()) { diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseDetailsPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseDetailsPage.java index 14f34f842f9d..ee6f48c5e2c6 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseDetailsPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseDetailsPage.java @@ -1,6 +1,6 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.Arrays; import java.util.List; diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEditPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEditPage.java index e1f0da0b5939..f0e1c4168495 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEditPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEditPage.java @@ -1,8 +1,8 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.HashMap; @@ -106,7 +106,7 @@ public void verifyInstructorDetails(InstructorAttributes instructor) { assertEquals("(This instructor will NOT be displayed to students)", getInstructorDisplayName(instrNum)); } assertEquals(instructor.getRole(), getInstructorRole(instrNum)); - if (instructor.getRole().equals(Const.InstructorPermissionRoleNames.INSTRUCTOR_PERMISSION_ROLE_CUSTOM) + if (Const.InstructorPermissionRoleNames.INSTRUCTOR_PERMISSION_ROLE_CUSTOM.equals(instructor.getRole()) && getEditInstructorButton(instrNum).isEnabled()) { verifyCustomPrivileges(instrNum, instructor.getPrivileges()); } @@ -305,7 +305,7 @@ public void editInstructor(int instrNum, InstructorAttributes instructor) { } public void toggleCustomCourseLevelPrivilege(int instrNum, String privilege) { - if (!getInstructorRole(instrNum).equals(Const.InstructorPermissionRoleNames.INSTRUCTOR_PERMISSION_ROLE_CUSTOM)) { + if (!Const.InstructorPermissionRoleNames.INSTRUCTOR_PERMISSION_ROLE_CUSTOM.equals(getInstructorRole(instrNum))) { return; } @@ -316,7 +316,7 @@ public void toggleCustomCourseLevelPrivilege(int instrNum, String privilege) { public void toggleCustomSectionLevelPrivilege(int instrNum, int panelNum, String section, String privilege) { - if (!getInstructorRole(instrNum).equals(Const.InstructorPermissionRoleNames.INSTRUCTOR_PERMISSION_ROLE_CUSTOM)) { + if (!Const.InstructorPermissionRoleNames.INSTRUCTOR_PERMISSION_ROLE_CUSTOM.equals(getInstructorRole(instrNum))) { return; } @@ -330,7 +330,7 @@ public void toggleCustomSectionLevelPrivilege(int instrNum, int panelNum, String public void toggleCustomSessionLevelPrivilege(int instrNum, int panelNum, String section, String session, String privilege) { - if (!getInstructorRole(instrNum).equals(Const.InstructorPermissionRoleNames.INSTRUCTOR_PERMISSION_ROLE_CUSTOM)) { + if (!Const.InstructorPermissionRoleNames.INSTRUCTOR_PERMISSION_ROLE_CUSTOM.equals(getInstructorRole(instrNum))) { return; } diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEnrollPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEnrollPage.java index d213d5448593..536791dc127b 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEnrollPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseEnrollPage.java @@ -1,6 +1,6 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.List; diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseStudentDetailsEditPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseStudentDetailsEditPage.java index 0aa807a0e2b0..200ceb38b35d 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseStudentDetailsEditPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseStudentDetailsEditPage.java @@ -1,6 +1,6 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseStudentDetailsViewPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseStudentDetailsViewPage.java index ebeb0a0ce3a5..b9b6eafa6fd0 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseStudentDetailsViewPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorCourseStudentDetailsViewPage.java @@ -1,6 +1,6 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackEditPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackEditPage.java index f104afcc7ba2..b00bf8fabfee 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackEditPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackEditPage.java @@ -1,8 +1,8 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.Instant; import java.time.ZoneId; @@ -674,6 +674,16 @@ public void addNumScaleQuestion(FeedbackQuestionAttributes feedbackQuestion) { clickSaveNewQuestionButton(); } + public void addNumScaleQuestion(FeedbackQuestion feedbackQuestion) { + addNewQuestion(5); + int questionNum = getNumQuestions(); + inputQuestionDetails(questionNum, feedbackQuestion); + FeedbackNumericalScaleQuestionDetails questionDetails = + (FeedbackNumericalScaleQuestionDetails) feedbackQuestion.getQuestionDetailsCopy(); + inputNumScaleDetails(questionNum, questionDetails); + clickSaveNewQuestionButton(); + } + public void editNumScaleQuestion(int questionNum, FeedbackNumericalScaleQuestionDetails questionDetails) { clickEditQuestionButton(questionNum); inputNumScaleDetails(questionNum, questionDetails); diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackResultsPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackResultsPage.java index e7210e7ae0be..b38f17539df9 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackResultsPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackResultsPage.java @@ -1,7 +1,7 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.Instant; import java.util.ArrayList; diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackSessionsPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackSessionsPage.java index 8dbff103bac1..ced3a6809047 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackSessionsPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorFeedbackSessionsPage.java @@ -1,6 +1,6 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.Instant; import java.time.ZoneId; diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorHomePage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorHomePage.java index 10dff2a76cf8..ab9ee676def0 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorHomePage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorHomePage.java @@ -1,6 +1,6 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.Instant; import java.util.List; @@ -12,6 +12,9 @@ import teammates.common.datatransfer.attributes.CourseAttributes; import teammates.common.datatransfer.attributes.FeedbackSessionAttributes; import teammates.common.datatransfer.attributes.StudentAttributes; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.Student; /** * Represents the instructor home page. @@ -38,12 +41,29 @@ public void verifyCourseTabDetails(int courseTabIndex, CourseAttributes course, verifyTableBodyValues(getSessionsTable(courseTabIndex), expectedValues); } + public void verifyCourseTabDetails(int courseTabIndex, Course course, FeedbackSession[] sessions) { + String expectedDetails = "[" + course.getId() + "]: " + course.getName(); + assertEquals(getCourseDetails(courseTabIndex), expectedDetails); + + String[][] expectedValues = new String[sessions.length][5]; + for (int i = 0; i < sessions.length; i++) { + expectedValues[i] = getExpectedSessionDetails(sessions[i]); + } + verifyTableBodyValues(getSessionsTable(courseTabIndex), expectedValues); + } + public void verifySessionDetails(int courseTabIndex, int sessionIndex, FeedbackSessionAttributes session) { String[] expectedValues = getExpectedSessionDetails(session); WebElement sessionRow = getSessionsTable(courseTabIndex).findElements(By.cssSelector("tbody tr")).get(sessionIndex); verifyTableRowValues(sessionRow, expectedValues); } + public void verifySessionDetails(int courseTabIndex, int sessionIndex, FeedbackSession session) { + String[] expectedValues = getExpectedSessionDetails(session); + WebElement sessionRow = getSessionsTable(courseTabIndex).findElements(By.cssSelector("tbody tr")).get(sessionIndex); + verifyTableRowValues(sessionRow, expectedValues); + } + public void verifyNumCourses(int expectedNum) { assertEquals(getNumCourses(), expectedNum); } @@ -59,6 +79,13 @@ public void copySession(int courseTabIndex, int sessionIndex, CourseAttributes c click(browser.driver.findElement(By.id("btn-confirm-copy-course"))); } + public void copySession(int courseTabIndex, int sessionIndex, Course copyToCourse, String newSessionName) { + WebElement copyFsModal = clickCopyButtonInTable(courseTabIndex, sessionIndex); + fillTextBox(copyFsModal.findElement(By.id("copy-session-name")), newSessionName); + selectCourseToCopyToInModal(copyFsModal, copyToCourse.getId()); + click(browser.driver.findElement(By.id("btn-confirm-copy-course"))); + } + public void publishSessionResults(int courseTabIndex, int sessionIndex) { WebElement courseTab = getCourseTab(courseTabIndex); click(courseTab.findElement(By.className("btn-results-" + sessionIndex))); @@ -85,6 +112,18 @@ public void sendReminderEmailToSelectedStudent(int courseTabIndex, int sessionIn click(courseTab.findElement(By.className("btn-remind-" + sessionIndex))); } + public void sendReminderEmailToSelectedStudent(int courseTabIndex, int sessionIndex, Student student) { + WebElement courseTab = getCourseTab(courseTabIndex); + click(courseTab.findElement(By.className("btn-remind-" + sessionIndex))); + List remindSelectedButtons = browser.driver.findElements( + By.className("btn-remind-selected-" + sessionIndex) + ); + click(remindSelectedButtons.get(remindSelectedButtons.size() - 1)); + selectStudentToEmail(student.getEmail()); + click(browser.driver.findElement(By.id("btn-confirm-send-reminder"))); + click(courseTab.findElement(By.className("btn-remind-" + sessionIndex))); + } + public void sendReminderEmailToNonSubmitters(int courseTabIndex, int sessionIndex) { WebElement courseTab = getCourseTab(courseTabIndex); click(courseTab.findElement(By.className("btn-remind-" + sessionIndex))); @@ -104,6 +143,14 @@ public void resendResultsLink(int courseTabIndex, int sessionIndex, StudentAttri click(browser.driver.findElement(By.id("btn-confirm-resend-results"))); } + public void resendResultsLink(int courseTabIndex, int sessionIndex, Student student) { + WebElement courseTab = getCourseTab(courseTabIndex); + click(courseTab.findElement(By.className("btn-results-" + sessionIndex))); + click(waitForElementPresence(By.className("btn-resend-" + sessionIndex))); + selectStudentToEmail(student.getEmail()); + click(browser.driver.findElement(By.id("btn-confirm-resend-results"))); + } + public void downloadResults(int courseTabIndex, int sessionIndex) { WebElement courseTab = getCourseTab(courseTabIndex); click(courseTab.findElement(By.className("btn-results-" + sessionIndex))); @@ -183,6 +230,23 @@ private String[] getExpectedSessionDetails(FeedbackSessionAttributes session) { return details; } + private String[] getExpectedSessionDetails(FeedbackSession session) { + String[] details = new String[5]; + details[0] = session.getName(); + details[1] = getDateString(session.getStartTime(), session.getCourse().getTimeZone()); + details[2] = getDateString(session.getEndTime(), session.getCourse().getTimeZone()); + + if (session.isClosed()) { + details[3] = "Closed"; + } else if (session.isVisible() && (session.isOpened() || session.isInGracePeriod())) { + details[3] = "Open"; + } else { + details[3] = "Awaiting"; + } + details[4] = session.isPublished() ? "Published" : "Not Published"; + return details; + } + private String getResponseRate(int courseTabIndex, int sessionIndex) { WebElement showButton = null; try { diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorSearchPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorSearchPage.java index f84b663a2d5b..50a6663446da 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorSearchPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorSearchPage.java @@ -1,7 +1,7 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import java.util.List; import java.util.Map; @@ -75,6 +75,7 @@ public void verifyStudentDetails(CourseAttributes course, StudentAttributes[] st WebElement targetCourse = getStudentTableForHeader(course); if (targetCourse == null) { fail("Course with ID " + course.getId() + " is not found"); + return; } WebElement studentList = targetCourse.findElement(By.tagName("table")); @@ -118,6 +119,7 @@ private WebElement getStudentRow(CourseAttributes course, String studentEmail) { WebElement targetCourse = getStudentTableForHeader(course); if (targetCourse == null) { fail("Course with ID " + course.getId() + " is not found"); + return null; } List studentRows = targetCourse.findElements(By.cssSelector("tbody tr")); diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorSessionIndividualExtensionPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorSessionIndividualExtensionPage.java index 3ecac1a73648..3e6f1f5be646 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorSessionIndividualExtensionPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorSessionIndividualExtensionPage.java @@ -1,6 +1,6 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.Duration; import java.time.Instant; diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorStudentListPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorStudentListPage.java index d33252527b76..8ac65b5d1237 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorStudentListPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorStudentListPage.java @@ -1,7 +1,7 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import java.util.Arrays; import java.util.Collection; @@ -67,6 +67,7 @@ public void verifyStudentDetails(CourseAttributes course, StudentAttributes[] st WebElement targetCourse = getCourseTab(course); if (targetCourse == null) { fail("Course with ID " + course.getId() + " is not found"); + return; } if (students.length == 0) { @@ -86,6 +87,7 @@ public void verifyStudentDetailsNotViewable(CourseAttributes course) { WebElement targetCourse = getCourseTab(course); if (targetCourse == null) { fail("Course with ID " + course.getId() + " is not found"); + return; } String noViewStudentsPermissionText = targetCourse.findElement(By.className("card-body")).getText(); String expectedText = "You do not have permission to view the details of the students in this course."; @@ -149,6 +151,7 @@ private WebElement getStudentRow(CourseAttributes course, String studentEmail) { WebElement targetCourse = getCourseTab(course); if (targetCourse == null) { fail("Course with ID " + course.getId() + " is not found"); + return null; } List studentRows = targetCourse.findElements(By.cssSelector("tbody tr")); diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorStudentListPageSql.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorStudentListPageSql.java new file mode 100644 index 000000000000..272a036e169b --- /dev/null +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorStudentListPageSql.java @@ -0,0 +1,199 @@ +package teammates.e2e.pageobjects; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import teammates.e2e.util.TestProperties; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.Student; +import teammates.test.ThreadHelper; + +/** + * Page Object Model for instructor student list page. + */ +public class InstructorStudentListPageSql extends AppPage { + + public InstructorStudentListPageSql(Browser browser) { + super(browser); + } + + @Override + protected boolean containsExpectedPageContents() { + return getPageSource().contains("Students"); + } + + private List getCoursesTabs() { + return browser.driver.findElements(By.className("course-table")); + } + + private String createHeaderText(Course course) { + return String.format("[%s]: %s", course.getId(), course.getName()); + } + + public void clickCourseTabHeader(Course course) { + String targetHeader = createHeaderText(course); + List courseTabs = getCoursesTabs(); + for (WebElement courseTab : courseTabs) { + WebElement headerElement = courseTab.findElement(By.className("card-header")); + String header = headerElement.getText(); + if (header.equals(targetHeader)) { + click(headerElement); + waitForPageToLoad(); + waitUntilAnimationFinish(); + } + } + } + + public void verifyAllCoursesHaveTabs(Collection courses) { + List courseTabs = getCoursesTabs(); + assertEquals(courses.size(), courseTabs.size()); + } + + public void verifyStudentDetails(Map courses, Map students) { + assertEquals(students.size(), courses.size()); + + students.forEach((courseId, studentsForCourse) -> verifyStudentDetails(courses.get(courseId), studentsForCourse)); + } + + public void verifyStudentDetails(Course course, Student[] students) { + WebElement targetCourse = getCourseTab(course); + if (targetCourse == null) { + fail("Course with ID " + course.getId() + " is not found"); + } + + if (students.length == 0) { + String noStudentText = targetCourse.findElement(By.className("card-body")).getText(); + // Need to account for the text from the enroll students button as well + String expectedText = "There are no students in this course." + + TestProperties.LINE_SEPARATOR + "Enroll Students"; + assertEquals(expectedText, noStudentText); + } else { + WebElement studentList = targetCourse.findElement(By.tagName("table")); + verifyTableBodyValues(studentList, getExpectedStudentValues(students)); + verifyDisplayedNumbers(targetCourse, students); + } + } + + public void verifyStudentDetailsNotViewable(Course course) { + WebElement targetCourse = getCourseTab(course); + if (targetCourse == null) { + fail("Course with ID " + course.getId() + " is not found"); + } + String noViewStudentsPermissionText = targetCourse.findElement(By.className("card-body")).getText(); + String expectedText = "You do not have permission to view the details of the students in this course."; + assertEquals(expectedText, noViewStudentsPermissionText); + } + + private WebElement getCourseTab(Course course) { + String targetHeader = createHeaderText(course); + List courseTabs = getCoursesTabs(); + + return courseTabs.stream().filter(courseTab -> { + String courseHeader = courseTab.findElement(By.className("card-header")).getText(); + return targetHeader.equals(courseHeader); + }).findFirst().orElse(null); + } + + private void verifyDisplayedNumbers(WebElement courseTab, Student[] students) { + String nStudents = courseTab.findElement(By.id("num-students")).getText(); + String nSections = courseTab.findElement(By.id("num-sections")).getText(); + String nTeams = courseTab.findElement(By.id("num-teams")).getText(); + + String expectedNStudents = students.length + " students"; + String expectedNSections = Arrays.stream(students) + .map(Student::getSection) + .distinct() + .count() + " sections"; + String expectedNTeams = Arrays.stream(students) + .map(Student::getTeam) + .distinct() + .count() + " teams"; + + assertEquals(expectedNStudents, nStudents); + assertEquals(expectedNSections, nSections); + assertEquals(expectedNTeams, nTeams); + } + + private String[][] getExpectedStudentValues(Student[] students) { + String[][] expected = new String[students.length][5]; + for (int i = 0; i < students.length; i++) { + Student student = students[i]; + expected[i][0] = student.getSectionName(); + expected[i][1] = student.getTeamName(); + expected[i][2] = student.getName(); + expected[i][3] = student.getGoogleId() == null ? "Yet to Join" : "Joined"; + expected[i][4] = student.getEmail(); + } + return expected; + } + + public void deleteStudent(Course course, String studentEmail) { + clickAndConfirm(getDeleteButton(course, studentEmail)); + waitUntilAnimationFinish(); + } + + private WebElement getDeleteButton(Course course, String studentEmail) { + WebElement studentRow = getStudentRow(course, studentEmail); + return studentRow.findElement(By.cssSelector("[id^='btn-delete-']")); + } + + private WebElement getStudentRow(Course course, String studentEmail) { + WebElement targetCourse = getCourseTab(course); + if (targetCourse == null) { + fail("Course with ID " + course.getId() + " is not found"); + } + + List studentRows = targetCourse.findElements(By.cssSelector("tbody tr")); + for (WebElement studentRow : studentRows) { + List studentCells = studentRow.findElements(By.tagName("td")); + if (studentCells.get(4).getText().equals(studentEmail)) { + return studentRow; + } + } + return null; + } + + public InstructorCourseEnrollPage clickEnrollStudents(Course course) { + WebElement studentRow = getCourseTab(course); + WebElement enrollButton = studentRow.findElement(By.id("btn-enroll")); + click(enrollButton); + waitForPageToLoad(); + return changePageType(InstructorCourseEnrollPage.class); + } + + public InstructorCourseStudentDetailsViewPage clickViewStudent(Course course, String studentEmail) { + WebElement studentRow = getStudentRow(course, studentEmail); + WebElement viewButton = studentRow.findElement(By.cssSelector("[id^='btn-view-details-']")); + click(viewButton); + ThreadHelper.waitFor(2000); + switchToNewWindow(); + return changePageType(InstructorCourseStudentDetailsViewPage.class); + } + + public InstructorCourseStudentDetailsEditPage clickEditStudent(Course course, String studentEmail) { + WebElement studentRow = getStudentRow(course, studentEmail); + WebElement viewButton = studentRow.findElement(By.cssSelector("[id^='btn-edit-details-']")); + click(viewButton); + ThreadHelper.waitFor(2000); + switchToNewWindow(); + return changePageType(InstructorCourseStudentDetailsEditPage.class); + } + + public InstructorStudentRecordsPage clickViewAllRecords(Course course, String studentEmail) { + WebElement studentRow = getStudentRow(course, studentEmail); + WebElement viewButton = studentRow.findElement(By.cssSelector("[id^='btn-view-records-']")); + click(viewButton); + ThreadHelper.waitFor(2000); + switchToNewWindow(); + return changePageType(InstructorStudentRecordsPage.class); + } + +} diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorStudentRecordsPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorStudentRecordsPage.java index d0c8a5e9920b..5f593bd4bee7 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorStudentRecordsPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorStudentRecordsPage.java @@ -1,11 +1,12 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import teammates.common.datatransfer.attributes.StudentAttributes; +import teammates.storage.sqlentity.Student; /** * Page Object Model for instructor student records page. @@ -33,4 +34,8 @@ public void verifyStudentDetails(StudentAttributes student) { verifyIsCorrectPage(student.getCourse(), student.getName()); } + public void verifyStudentDetails(Student student) { + verifyIsCorrectPage(student.getCourse().getId(), student.getName()); + } + } diff --git a/src/e2e/java/teammates/e2e/pageobjects/RequestPage.java b/src/e2e/java/teammates/e2e/pageobjects/RequestPage.java new file mode 100644 index 000000000000..05ef23a56e03 --- /dev/null +++ b/src/e2e/java/teammates/e2e/pageobjects/RequestPage.java @@ -0,0 +1,71 @@ +package teammates.e2e.pageobjects; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * Page Object Model for account request form page. + */ +public class RequestPage extends AppPage { + + @FindBy(id = "btn-am-instructor") + private WebElement amInstructorButton; + + @FindBy(id = "name") + private WebElement nameBox; + + @FindBy(id = "institution") + private WebElement institutionBox; + + @FindBy(id = "country") + private WebElement countryBox; + + @FindBy(id = "email") + private WebElement emailBox; + + @FindBy(id = "comments") + private WebElement commentsBox; + + @FindBy(id = "submit-button") + private WebElement submitButton; + + public RequestPage(Browser browser) { + super(browser); + } + + @Override + protected boolean containsExpectedPageContents() { + return getPageTitle().contains("Request for an Instructor Account"); + } + + public void clickAmInstructorButton() { + click(amInstructorButton); + waitForPageToLoad(); + } + + public void fillForm(String name, String institution, String country, String email, String comments) { + fillTextBox(nameBox, name); + fillTextBox(institutionBox, institution); + fillTextBox(countryBox, country); + fillTextBox(emailBox, email); + fillTextBox(commentsBox, comments); + } + + public void clickSubmitFormButton() { + click(submitButton); + waitForPageToLoad(); + } + + public void verifySubmittedInfo(String name, String institution, String country, String email, String comments) { + WebElement table = browser.driver.findElement(By.className("table")); + String[][] expected = { + { name }, + { institution }, + { country }, + { email }, + { comments }, + }; + verifyTableBodyValues(table, expected); + } +} diff --git a/src/e2e/java/teammates/e2e/pageobjects/StudentCourseDetailsPage.java b/src/e2e/java/teammates/e2e/pageobjects/StudentCourseDetailsPage.java index 40d487a74240..43f7cec0b6d9 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/StudentCourseDetailsPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/StudentCourseDetailsPage.java @@ -1,6 +1,6 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.ArrayList; import java.util.List; @@ -14,6 +14,9 @@ import teammates.common.datatransfer.attributes.InstructorAttributes; import teammates.common.datatransfer.attributes.StudentAttributes; import teammates.e2e.util.TestProperties; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.Instructor; +import teammates.storage.sqlentity.Student; /** * Page Object Model for student course details page. @@ -59,6 +62,12 @@ public void verifyCourseDetails(CourseAttributes courseDetails) { assertEquals(courseDetails.getInstitute(), courseInstituteField.getText()); } + public void verifyCourseDetails(Course courseDetails) { + assertEquals(courseDetails.getName(), courseNameField.getText()); + assertEquals(courseDetails.getId(), courseIdField.getText()); + assertEquals(courseDetails.getInstitute(), courseInstituteField.getText()); + } + public void verifyInstructorsDetails(InstructorAttributes[] instructorDetails) { String[] actualInstructors = instructorsList.getText().split(TestProperties.LINE_SEPARATOR); for (int i = 0; i < instructorDetails.length; i++) { @@ -68,6 +77,15 @@ public void verifyInstructorsDetails(InstructorAttributes[] instructorDetails) { } } + public void verifyInstructorsDetails(Instructor[] instructorDetails) { + String[] actualInstructors = instructorsList.getText().split(TestProperties.LINE_SEPARATOR); + for (int i = 0; i < instructorDetails.length; i++) { + Instructor expected = instructorDetails[i]; + assertEquals(expected.getDisplayName() + ": " + expected.getName() + " (" + expected.getEmail() + ")", + actualInstructors[i]); + } + } + public void verifyStudentDetails(StudentAttributes studentDetails) { assertEquals(studentDetails.getName(), studentNameField.getText()); assertEquals(studentDetails.getSection(), studentSectionField.getText()); @@ -75,6 +93,13 @@ public void verifyStudentDetails(StudentAttributes studentDetails) { assertEquals(studentDetails.getEmail(), studentEmailField.getText()); } + public void verifyStudentDetails(Student studentDetails) { + assertEquals(studentDetails.getName(), studentNameField.getText()); + assertEquals(studentDetails.getSectionName(), studentSectionField.getText()); + assertEquals(studentDetails.getTeamName(), studentTeamField.getText()); + assertEquals(studentDetails.getEmail(), studentEmailField.getText()); + } + public void verifyTeammatesDetails(StudentAttributes[] teammates) { int numTables = teammates.length; @@ -89,6 +114,20 @@ public void verifyTeammatesDetails(StudentAttributes[] teammates) { } } + public void verifyTeammatesDetails(Student[] teammates) { + int numTables = teammates.length; + + for (int i = 0; i < numTables; i++) { + List profileItems = new ArrayList<>(); + profileItems.add("Name: " + teammates[i].getName()); + profileItems.add("Email: " + teammates[i].getEmail()); + + WebElement actualProfile = browser.driver.findElement(By.id("teammates-details-" + i)); + assertEquals(profileItems.stream().collect(Collectors.joining(TestProperties.LINE_SEPARATOR)), + actualProfile.getText()); + } + } + public void sortTeammatesByName() { click(browser.driver.findElement(By.id("sort-name"))); } diff --git a/src/e2e/java/teammates/e2e/pageobjects/StudentHomePage.java b/src/e2e/java/teammates/e2e/pageobjects/StudentHomePage.java index b1474312004a..62d183e12315 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/StudentHomePage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/StudentHomePage.java @@ -1,6 +1,6 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; diff --git a/src/e2e/java/teammates/e2e/pageobjects/UserNotificationsPage.java b/src/e2e/java/teammates/e2e/pageobjects/UserNotificationsPage.java index 1460a699c524..d1bc47381eb5 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/UserNotificationsPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/UserNotificationsPage.java @@ -1,8 +1,8 @@ package teammates.e2e.pageobjects; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.Instant; import java.util.List; diff --git a/src/e2e/java/teammates/e2e/util/TestDataValidityTest.java b/src/e2e/java/teammates/e2e/util/TestDataValidityTest.java index d63cd4d60f3d..99af4d002c30 100644 --- a/src/e2e/java/teammates/e2e/util/TestDataValidityTest.java +++ b/src/e2e/java/teammates/e2e/util/TestDataValidityTest.java @@ -181,7 +181,7 @@ private boolean isValidTestCourseId(String courseId, String testPage) { } private boolean isValidTestGoogleId(String googleId, String testPage) { - if (googleId == null || googleId.equals("")) { + if (googleId == null || "".equals(googleId)) { // Empty google ID is always acceptable return true; } diff --git a/src/e2e/java/teammates/e2e/util/TestProperties.java b/src/e2e/java/teammates/e2e/util/TestProperties.java index ecdd4d3f648b..9279aaf437cd 100644 --- a/src/e2e/java/teammates/e2e/util/TestProperties.java +++ b/src/e2e/java/teammates/e2e/util/TestProperties.java @@ -9,6 +9,7 @@ /** * Represents properties in test.properties file. */ +@SuppressWarnings("PMD.TestClassWithoutTestCases") public final class TestProperties { /** The directory where JSON files used to create data bundles are stored. */ diff --git a/src/e2e/resources/data/AdminSearchPageE2ESqlTest.json b/src/e2e/resources/data/AdminSearchPageE2ESqlTest.json new file mode 100644 index 000000000000..3a36ff511d8d --- /dev/null +++ b/src/e2e/resources/data/AdminSearchPageE2ESqlTest.json @@ -0,0 +1,147 @@ +{ + "accounts": { + "instructor1OfCourse1": { + "id": "00000000-0000-4000-8000-000000000001", + "googleId": "tm.e2e.ASearch.instr1", + "name": "Instructor1 of Course1", + "email": "ASearch.instructor1@gmail.tmt" + }, + "instructor2OfCourse1": { + "id": "00000000-0000-4000-8000-000000000002", + "googleId": "tm.e2e.ASearch.instr2", + "name": "Instructor2 of Course1", + "email": "ASearch.instructor2@gmail.tmt" + }, + "student1InCourse1": { + "id": "00000000-0000-4000-8000-000000000003", + "googleId": "tm.e2e.ASearch.student1", + "name": "Student1 in course1", + "email": "ASearch.student@gmail.tmt" + } + }, + "accountRequests": { + "instructor1OfCourse1": { + "name": "Instructor1 of Course1", + "email": "ASearch.instructor1@gmail.tmt", + "institute": "TEAMMATES Test Institute 1", + "createdAt": "2011-01-01T00:00:00Z", + "registeredAt": "1970-02-14T00:00:00Z" + }, + "instructor2OfCourse1": { + "name": "Instructor2 of Course1", + "email": "ASearch.instructor2@gmail.tmt", + "institute": "TEAMMATES Test Institute 1", + "createdAt": "2011-01-01T00:00:00Z", + "registeredAt": "1970-02-14T00:00:00Z" + }, + "unregisteredInstructor1": { + "name": "Typical Instructor Name", + "email": "ASearch.unregisteredinstructor1@gmail.tmt", + "institute": "TEAMMATES Test Institute 1", + "createdAt": "2011-01-01T00:00:00Z", + "status": "PENDING" + }, + "unregisteredInstructor2": { + "name": "Typical Instructor Name", + "email": "ASearch.unregisteredinstructor2@gmail.tmt", + "institute": "TEAMMATES Test Institute 2", + "createdAt": "2011-01-01T00:00:00Z", + "status": "PENDING" + }, + "unregisteredInstructor3": { + "name": "Typical Instructor Name", + "email": "ASearch.unregisteredinstructor3@gmail.tmt", + "institute": "TEAMMATES Test Institute 3", + "createdAt": "2011-01-01T00:00:00Z", + "status": "PENDING" + }, + "unregisteredInstructor4": { + "name": "Typical Instructor Name", + "email": "ASearch.unregisteredinstructor4@gmail.tmt", + "institute": "TEAMMATES Test Institute 4", + "createdAt": "2011-01-01T00:00:00Z", + "status": "PENDING" + }, + "unregisteredInstructor5": { + "name": "Typical Instructor Name", + "email": "ASearch.unregisteredinstructor5@gmail.tmt", + "institute": "TEAMMATES Test Institute 5", + "createdAt": "2011-01-01T00:00:00Z", + "status": "PENDING" + } + }, + "courses": { + "typicalCourse1": { + "createdAt": "2012-04-01T23:59:00Z", + "id": "00000000-0000-4000-8000-000000000303", + "name": "ASearch Course 1", + "institute": "TEAMMATES Test Institute 0", + "timeZone": "Africa/Johannesburg" + } + }, + "sections": { + "section1InCourse1": { + "id": "00000000-0000-4000-8000-000000000201", + "course": { + "id": "00000000-0000-4000-8000-000000000303" + }, + "name": "Section 1" + } + }, + "teams": { + "team1InCourse1": { + "id": "00000000-0000-4000-8000-000000000301", + "section": { + "id": "00000000-0000-4000-8000-000000000201" + }, + "name": "Team 1" + } + }, + "instructors": { + "instructor1OfCourse1": { + "id": "00000000-0000-4000-8000-000000000501", + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "course": { + "id": "00000000-0000-4000-8000-000000000303" + }, + "name": "Instructor1 of ASearch Course1", + "email": "ASearch.instructor@gmail.tmt", + "role": "INSTRUCTOR_PERMISSION_ROLE_COOWNER", + "isDisplayedToStudents": true, + "displayName": "Instructor", + "privileges": { + "courseLevel": { + "canModifyCourse": true, + "canModifyInstructor": true, + "canModifySession": true, + "canModifyStudent": true, + "canViewStudentInSections": true, + "canViewSessionInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true + }, + "sectionLevel": {}, + "sessionLevel": {} + } + } + }, + "students": { + "student1InCourse1": { + "id": "00000000-0000-4000-8000-000000000601", + "account": { + "id": "00000000-0000-4000-8000-000000000003" + }, + "course": { + "id": "00000000-0000-4000-8000-000000000303" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000301" + }, + "email": "ASearch.student@gmail.tmt", + "name": "Student1 In ASearch Course1", + "comments": "comment for student1Course1" + } + } +} diff --git a/src/e2e/resources/data/FeedbackNumScaleQuestionE2ESqlTest.json b/src/e2e/resources/data/FeedbackNumScaleQuestionE2ESqlTest.json new file mode 100644 index 000000000000..908ecff638c6 --- /dev/null +++ b/src/e2e/resources/data/FeedbackNumScaleQuestionE2ESqlTest.json @@ -0,0 +1,268 @@ +{ + "accounts": { + "instructorWithSessions": { + "googleId": "tm.e2e.FNumScaleQn.instructor", + "name": "Teammates Test", + "email": "tmms.test@gmail.tmt", + "id": "00000000-0000-4000-8000-000000000001" + }, + "tm.e2e.FNumScaleQn.alice.tmms": { + "googleId": "tm.e2e.FNumScaleQn.alice.tmms", + "name": "Alice Betsy", + "email": "alice.b.tmms@gmail.tmt", + "id": "00000000-0000-4000-8000-000000000002" + } + }, + "accountRequests": {}, + "courses": { + "course": { + "id": "tm.e2e.FNumScaleQn.CS2104", + "name": "Programming Language Concepts", + "institute": "TEAMMATES Test Institute 1", + "timeZone": "Africa/Johannesburg" + }, + "course2": { + "id": "tm.e2e.FNumScaleQn.CS1101", + "name": "Programming Methodology", + "institute": "TEAMMATES Test Institute 1", + "timeZone": "Africa/Johannesburg" + } + }, + "instructors": { + "instructor": { + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "name": "Teammates Test", + "email": "tmms.test@gmail.tmt", + "role": "INSTRUCTOR_PERMISSION_ROLE_COOWNER", + "isDisplayedToStudents": true, + "privileges": { + "courseLevel": { + "canViewStudentInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true, + "canModifyCourse": true, + "canViewSessionInSections": true, + "canModifySession": true, + "canModifyStudent": true, + "canModifyInstructor": true + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "id": "00000000-0000-4000-8000-000000000501", + "course": { + "id": "tm.e2e.FNumScaleQn.CS2104" + }, + "displayName": "Co-owner" + }, + "instructor2": { + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "name": "Teammates Test", + "email": "tmms.test@gmail.tmt", + "role": "INSTRUCTOR_PERMISSION_ROLE_COOWNER", + "isDisplayedToStudents": true, + "privileges": { + "courseLevel": { + "canViewStudentInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true, + "canModifyCourse": true, + "canViewSessionInSections": true, + "canModifySession": true, + "canModifyStudent": true, + "canModifyInstructor": true + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "id": "00000000-0000-4000-8000-000000000502", + "course": { + "id": "tm.e2e.FNumScaleQn.CS1101" + }, + "displayName": "Co-owner" + } + }, + "sections": { + "ProgrammingLanguageConceptsNone": { + "id": "00000000-0000-4000-8000-000000000101", + "course": { + "id": "tm.e2e.FNumScaleQn.CS2104" + }, + "name": "None" + }, + "ProgrammingMethodologyNone": { + "id": "00000000-0000-4000-8000-000000000102", + "course": { + "id": "tm.e2e.FNumScaleQn.CS1101" + }, + "name": "None" + } + }, + "teams": { + "ProgrammingLanguageConceptsTeam1": { + "id": "00000000-0000-4000-8000-000000000201", + "section": { + "id": "00000000-0000-4000-8000-000000000101" + }, + "name": "Team 1" + }, + "ProgrammingLanguageConceptsTeam2": { + "id": "00000000-0000-4000-8000-000000000202", + "section": { + "id": "00000000-0000-4000-8000-000000000101" + }, + "name": "Team 2" + }, + "ProgrammingMethodologyTeam1": { + "id": "00000000-0000-4000-8000-000000000203", + "section": { + "id": "00000000-0000-4000-8000-000000000102" + }, + "name": "Team 1" + } + }, + "feedbackSessions": { + "openSession": { + "creatorEmail": "tmms.test@gmail.tmt", + "instructions": "

Instructions for first session

", + "createdTime": "2012-04-01T23:59:00Z", + "startTime": "2012-04-01T22:00:00Z", + "endTime": "2026-04-30T22:00:00Z", + "sessionVisibleFromTime": "2012-04-01T22:00:00Z", + "resultsVisibleFromTime": "2026-05-01T22:00:00Z", + "timeZone": "Africa/Johannesburg", + "gracePeriod": 10, + "sentOpenEmail": false, + "sentClosingEmail": false, + "sentClosedEmail": false, + "sentPublishedEmail": false, + "isOpeningEmailEnabled": true, + "isClosingEmailEnabled": true, + "isPublishedEmailEnabled": true, + "studentDeadlines": {}, + "instructorDeadlines": {}, + "id": "00000000-0000-4000-8000-000000000701", + "course": { + "id": "tm.e2e.FNumScaleQn.CS2104" + }, + "name": "First Session" + }, + "openSession2": { + "creatorEmail": "tmms.test@gmail.tmt", + "instructions": "

Instructions for second session

", + "createdTime": "2012-04-01T23:59:00Z", + "startTime": "2012-04-01T22:00:00Z", + "endTime": "2026-04-30T22:00:00Z", + "sessionVisibleFromTime": "2012-04-01T22:00:00Z", + "resultsVisibleFromTime": "2026-05-01T22:00:00Z", + "timeZone": "Africa/Johannesburg", + "gracePeriod": 10, + "sentOpenEmail": false, + "sentClosingEmail": false, + "sentClosedEmail": false, + "sentPublishedEmail": false, + "isOpeningEmailEnabled": true, + "isClosingEmailEnabled": true, + "isPublishedEmailEnabled": true, + "studentDeadlines": {}, + "instructorDeadlines": {}, + "id": "00000000-0000-4000-8000-000000000702", + "course": { + "id": "tm.e2e.FNumScaleQn.CS1101" + }, + "name": "Second Session" + } + }, + "feedbackQuestions": { + "qn1ForFirstSession": { + "id": "00000000-0000-4000-8000-000000000801", + "feedbackSession": { + "id": "00000000-0000-4000-8000-000000000701" + }, + "questionDetails": { + "questionType": "NUMSCALE", + "questionText": "Rate this team's product", + "minScale": 0, + "maxScale": 10, + "step": 0.2 + }, + "description": "

Testing description for first session

", + "questionNumber": 1, + "giverType": "STUDENTS", + "recipientType": "TEAMS_EXCLUDING_SELF", + "numOfEntitiesToGiveFeedbackTo": 1, + "showResponsesTo": ["INSTRUCTORS", "RECEIVER"], + "showGiverNameTo": ["INSTRUCTORS"], + "showRecipientNameTo": ["INSTRUCTORS", "RECEIVER"] + }, + "qn1ForSecondSession": { + "id": "00000000-0000-4000-8000-000000000802", + "feedbackSession": { + "id": "00000000-0000-4000-8000-000000000702" + }, + "questionDetails": { + "questionType": "NUMSCALE", + "questionText": "Rate this team's teamwork", + "minScale": 1, + "maxScale": 10, + "step": 0.005 + }, + "description": "

Testing description for second session

", + "questionNumber": 1, + "giverType": "STUDENTS", + "recipientType": "TEAMS_EXCLUDING_SELF", + "numOfEntitiesToGiveFeedbackTo": 1, + "showResponsesTo": ["INSTRUCTORS", "RECEIVER"], + "showGiverNameTo": ["INSTRUCTORS"], + "showRecipientNameTo": ["INSTRUCTORS", "RECEIVER"] + } + }, + "notifications": {}, + "readNotifications": {}, + "feedbackResponseComments": {}, + "students": { + "alice.tmms@FNumScaleQn.CS2104": { + "id": "00000000-0000-4000-8000-000000000601", + "account": { + "id": "00000000-0000-4000-8000-000000000002" + }, + "course": { + "id": "tm.e2e.FNumScaleQn.CS2104" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000201" + }, + "email": "alice.b.tmms@gmail.tmt", + "name": "Alice Betsy", + "comments": "This student's name is Alice Betsy" + }, + "benny.tmms@FNumScaleQn.CS2104": { + "id": "00000000-0000-4000-8000-000000000602", + "course": { + "id": "tm.e2e.FNumScaleQn.CS2104" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000202" + }, + "email": "benny.tmms@gmail.tmt", + "name": "Benny Charles", + "comments": "This student's name is Benny Charles" + }, + "charlie.tmms@FNumScaleQn.CS1101": { + "id": "00000000-0000-4000-8000-000000000603", + "course": { + "id": "tm.e2e.FNumScaleQn.CS1101" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000203" + }, + "email": "charlie.tmms@gmail.tmt", + "name": "Charlie Davis", + "comments": "This student's name is Charlie Davis" + } + } +} diff --git a/src/e2e/resources/data/InstructorHomePageE2ETestSql.json b/src/e2e/resources/data/InstructorHomePageE2ETestSql.json new file mode 100644 index 000000000000..ec5fa8858b77 --- /dev/null +++ b/src/e2e/resources/data/InstructorHomePageE2ETestSql.json @@ -0,0 +1,452 @@ +{ + "accounts": { + "IHome.instr": { + "id": "00000000-0000-4000-8000-000000000001", + "googleId": "tm.e2e.IHome.instructor.tmms", + "name": "Teammates Test", + "email": "IHome.instructor.tmms@gmail.tmt" + } + }, + "accountRequests": {}, + "courses": { + "IHome.CS2104": { + "id": "tm.e2e.IHome.CS2104", + "name": "Programming Language Concepts", + "timeZone": "Asia/Singapore", + "institute": "TEAMMATES Test Institute 1", + "createdAt": "2012-04-01T23:58:00Z" + }, + "IHome.CS1101": { + "id": "tm.e2e.IHome.CS1101", + "name": "Programming Methodology", + "timeZone": "Asia/Singapore", + "institute": "TEAMMATES Test Institute 1", + "createdAt": "2013-04-01T23:59:00Z" + } + }, + "sections": { + "tm.e2e.IHome.CS2104-None": { + "id": "00000000-0000-4000-8000-000000000201", + "course": { + "id": "tm.e2e.IHome.CS2104" + }, + "name": "None" + }, + "tm.e2e.IHome.CS1101-None": { + "id": "00000000-0000-4000-8000-000000000202", + "course": { + "id": "tm.e2e.IHome.CS1101" + }, + "name": "None" + } + }, + "teams": { + "tm.e2e.IHome.CS2104-None-Team1": { + "id": "00000000-0000-4000-8000-000000000301", + "section": { + "id": "00000000-0000-4000-8000-000000000201" + }, + "name": "Team 1" + }, + "tm.e2e.IHome.CS2104-None-Team2": { + "id": "00000000-0000-4000-8000-000000000302", + "section": { + "id": "00000000-0000-4000-8000-000000000201" + }, + "name": "Team 2" + }, + "tm.e2e.IHome.CS1101-None-Team1": { + "id": "00000000-0000-4000-8000-000000000303", + "section": { + "id": "00000000-0000-4000-8000-000000000202" + }, + "name": "Team 1" + }, + "tm.e2e.IHome.CS1101-None-Team2": { + "id": "00000000-0000-4000-8000-000000000304", + "section": { + "id": "00000000-0000-4000-8000-000000000202" + }, + "name": "Team 2" + } + }, + "deadlineExtensions": {}, + "instructors": { + "IHome.instr.CS2104": { + "isDisplayedToStudents": true, + "displayName": "Instructor", + "role": "INSTRUCTOR_PERMISSION_ROLE_COOWNER", + "privileges": { + "courseLevel": { + "canModifyCourse": true, + "canModifyInstructor": true, + "canModifySession": true, + "canModifyStudent": true, + "canViewStudentInSections": true, + "canViewSessionInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "id": "00000000-0000-4000-8000-000000000501", + "courseId": "tm.e2e.IHome.CS2104", + "course": { + "id": "tm.e2e.IHome.CS2104" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "name": "Teammates Test", + "email": "IHome.instructor.tmms@gmail.tmt" + }, + "IHome.instr.CS1101": { + "isDisplayedToStudents": true, + "displayName": "Instructor", + "role": "INSTRUCTOR_PERMISSION_ROLE_COOWNER", + "privileges": { + "courseLevel": { + "canModifyCourse": true, + "canModifyInstructor": true, + "canModifySession": true, + "canModifyStudent": true, + "canViewStudentInSections": true, + "canViewSessionInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "id": "00000000-0000-4000-8000-000000000502", + "courseId": "tm.e2e.IHome.CS1101", + "course": { + "id": "tm.e2e.IHome.CS1101" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "name": "Teammates Test", + "email": "IHome.instructor.tmms@gmail.tmt" + } + }, + "students": { + "IHome.alice.b.tmms@IHome.CS2104": { + "comments": "This student's name is Alice Betsy", + "id": "00000000-0000-4000-8000-000000000601", + "courseId": "tm.e2e.IHome.CS2104", + "course": { + "id": "tm.e2e.IHome.CS2104" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000301" + }, + "name": "Alice Betsy", + "email": "IHome.alice.b.tmms@gmail.tmt" + }, + "IHome.benny.c.tmms@IHome.CS2104": { + "comments": "This student's name is Benny Charles", + "id": "00000000-0000-4000-8000-000000000602", + "courseId": "tm.e2e.IHome.CS2104", + "course": { + "id": "tm.e2e.IHome.CS2104" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000301" + }, + "name": "Benny Charles", + "email": "IHome.benny.c.tmms@gmail.tmt" + }, + "IHome.charlie.d.tmms@IHome.CS2104": { + "comments": "This student's name is Charlie Davis", + "id": "00000000-0000-4000-8000-000000000603", + "courseId": "tm.e2e.IHome.CS2104", + "course": { + "id": "tm.e2e.IHome.CS2104" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000302" + }, + "name": "Charlie Davis", + "email": "IHome.charlie.d.tmms@gmail.tmt" + }, + "IHome.danny.e.tmms@IHome.CS2104": { + "comments": "This student's name is Danny Engrid", + "id": "00000000-0000-4000-8000-000000000604", + "courseId": "tm.e2e.IHome.CS2104", + "course": { + "id": "tm.e2e.IHome.CS2104" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000302" + }, + "name": "Danny Engrid", + "email": "IHome.danny.e.tmms@gmail.tmt" + }, + "IHome.alice.b.tmms@IHome.CS1101": { + "comments": "This student's name is Alice Betsy", + "id": "00000000-0000-4000-8000-000000000605", + "courseId": "tm.e2e.IHome.CS1101", + "course": { + "id": "tm.e2e.IHome.CS1101" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000303" + }, + "name": "Alice Betsy", + "email": "IHome.alice.b.tmms@gmail.tmt" + }, + "IHome.benny.c.tmms@IHome.CS1101": { + "comments": "This student's name is Benny Charles", + "id": "00000000-0000-4000-8000-000000000606", + "courseId": "tm.e2e.IHome.CS1101", + "course": { + "id": "tm.e2e.IHome.CS1101" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000303" + }, + "name": "Benny Charles", + "email": "IHome.benny.c.tmms@gmail.tmt" + }, + "IHome.charlie.d.tmms@IHome.CS1101": { + "comments": "This student's name is Charlie Davis", + "id": "00000000-0000-4000-8000-000000000607", + "courseId": "tm.e2e.IHome.CS1101", + "course": { + "id": "tm.e2e.IHome.CS1101" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000304" + }, + "name": "Charlie Davis", + "email": "IHome.charlie.d.tmms@gmail.tmt" + }, + "IHome.danny.e.tmms@IHome.CS1101": { + "comments": "This student's name is Danny Engrid", + "id": "00000000-0000-4000-8000-000000000608", + "courseId": "tm.e2e.IHome.CS1101", + "course": { + "id": "tm.e2e.IHome.CS1101" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000304" + }, + "name": "Danny Engrid", + "email": "IHome.danny.e.tmms@gmail.tmt" + } + }, + "feedbackSessions": { + "First Feedback Session": { + "id": "00000000-0000-4000-8000-000000000701", + "course": { + "id": "tm.e2e.IHome.CS2104" + }, + "name": "First Feedback Session", + "creatorEmail": "IHome.instructor.tmms@gmail.tmt", + "instructions": "Please please fill in the following questions.", + "startTime": "2012-04-01T04:00:00Z", + "endTime": "2027-04-30T16:00:00Z", + "sessionVisibleFromTime": "2012-03-28T16:00:00Z", + "resultsVisibleFromTime": "2027-05-01T16:00:00Z", + "gracePeriod": 10, + "isOpeningEmailEnabled": true, + "isClosingEmailEnabled": true, + "isPublishedEmailEnabled": true, + "isOpeningSoonEmailSent": false, + "isOpenEmailSent": false, + "isClosingSoonEmailSent": false, + "isClosedEmailSent": false, + "isPublishedEmailSent": false, + "createdAt": "2012-03-20T23:59:00Z" + }, + "Second Feedback Session": { + "id": "00000000-0000-4000-8000-000000000702", + "course": { + "id": "tm.e2e.IHome.CS2104" + }, + "name": "Second Feedback Session", + "creatorEmail": "IHome.instructor.tmms@gmail.tmt", + "instructions": "Please please fill in the following questions.", + "startTime": "2012-03-29T04:00:00Z", + "endTime": "2012-05-01T04:00:00Z", + "sessionVisibleFromTime": "2012-03-28T16:00:00Z", + "resultsVisibleFromTime": "2012-05-01T16:00:00Z", + "gracePeriod": 10, + "isOpeningEmailEnabled": true, + "isClosingEmailEnabled": true, + "isPublishedEmailEnabled": true, + "isOpeningSoonEmailSent": false, + "isOpenEmailSent": false, + "isClosingSoonEmailSent": false, + "isClosedEmailSent": false, + "isPublishedEmailSent": false, + "createdAt": "2012-03-20T23:59:00Z" + }, + "Third Feedback Session": { + "id": "00000000-0000-4000-8000-000000000703", + "course": { + "id": "tm.e2e.IHome.CS2104" + }, + "name": "Third Feedback Session", + "creatorEmail": "IHome.instructor.tmms@gmail.tmt", + "instructions": "Please please fill in the following questions.", + "startTime": "2012-04-10T16:00:00Z", + "endTime": "2012-04-30T16:00:00Z", + "sessionVisibleFromTime": "2012-03-28T16:00:00Z", + "resultsVisibleFromTime": "2027-05-01T16:00:00Z", + "gracePeriod": 10, + "isOpeningEmailEnabled": true, + "isClosingEmailEnabled": true, + "isPublishedEmailEnabled": true, + "isOpeningSoonEmailSent": false, + "isOpenEmailSent": false, + "isClosingSoonEmailSent": false, + "isClosedEmailSent": false, + "isPublishedEmailSent": false, + "createdAt": "2012-03-20T23:59:00Z" + }, + "Fourth Feedback Session": { + "id": "00000000-0000-4000-8000-000000000704", + "course": { + "id": "tm.e2e.IHome.CS2104" + }, + "name": "Fourth Feedback Session", + "creatorEmail": "IHome.instructor.tmms@gmail.tmt", + "instructions": "Please please fill in the following questions.", + "startTime": "2012-04-05T04:00:00Z", + "endTime": "2012-04-20T04:00:00Z", + "sessionVisibleFromTime": "2012-03-28T16:00:00Z", + "resultsVisibleFromTime": "2012-05-01T16:00:00Z", + "gracePeriod": 10, + "isOpeningEmailEnabled": true, + "isClosingEmailEnabled": true, + "isPublishedEmailEnabled": true, + "isOpeningSoonEmailSent": false, + "isOpenEmailSent": false, + "isClosingSoonEmailSent": false, + "isClosedEmailSent": false, + "isPublishedEmailSent": false, + "createdAt": "2012-03-20T23:59:00Z" + }, + "CS1101 Session": { + "id": "00000000-0000-4000-8000-000000000705", + "course": { + "id": "tm.e2e.IHome.CS1101" + }, + "name": "CS1101 Session", + "creatorEmail": "IHome.instructor.tmms@gmail.tmt", + "instructions": "Please please fill in the following questions.", + "startTime": "2012-04-05T04:00:00Z", + "endTime": "2012-04-20T04:00:00Z", + "sessionVisibleFromTime": "2012-03-28T16:00:00Z", + "resultsVisibleFromTime": "2012-05-01T16:00:00Z", + "gracePeriod": 10, + "isOpeningEmailEnabled": true, + "isClosingEmailEnabled": true, + "isPublishedEmailEnabled": true, + "isOpeningSoonEmailSent": false, + "isOpenEmailSent": false, + "isClosingSoonEmailSent": false, + "isClosedEmailSent": false, + "isPublishedEmailSent": false, + "createdAt": "2012-03-20T23:59:00Z" + } + }, + "feedbackQuestions": { + "IHome.CS2104:First Feedback Session:Q1": { + "questionDetails": { + "shouldAllowRichText": true, + "questionType": "TEXT", + "questionText": "Rate 5 other students' products" + }, + "id": "00000000-0000-4000-8000-000000000801", + "feedbackSession": { + "id": "00000000-0000-4000-8000-000000000701" + }, + "description": "Feedback text question.", + "questionNumber": 1, + "giverType": "STUDENTS", + "recipientType": "STUDENTS", + "numOfEntitiesToGiveFeedbackTo": 5, + "showResponsesTo": [ + "INSTRUCTORS", + "RECEIVER" + ], + "showGiverNameTo": [ + "INSTRUCTORS" + ], + "showRecipientNameTo": [ + "INSTRUCTORS", + "RECEIVER" + ] + }, + "IHome.CS2104:Third Feedback Session:Q1": { + "questionDetails": { + "shouldAllowRichText": true, + "questionType": "TEXT", + "questionText": "Rate 5 other students' products" + }, + "id": "00000000-0000-4000-8000-000000000802", + "feedbackSession": { + "id": "00000000-0000-4000-8000-000000000703" + }, + "description": "Feedback text question.", + "questionNumber": 1, + "giverType": "STUDENTS", + "recipientType": "STUDENTS", + "numOfEntitiesToGiveFeedbackTo": 5, + "showResponsesTo": [ + "INSTRUCTORS", + "RECEIVER" + ], + "showGiverNameTo": [ + "INSTRUCTORS" + ], + "showRecipientNameTo": [ + "INSTRUCTORS", + "RECEIVER" + ] + } + }, + "feedbackResponses": { + "response1": { + "answer": { + "answer": "Alice response to Danny.", + "questionType": "TEXT" + }, + "id": "00000000-0000-4000-8000-000000000901", + "giver": "IHome.alice.b.tmms@gmail.tmt", + "giverSection": { + "id": "00000000-0000-4000-8000-000000000201" + }, + "recipient": "IHome.danny.e.tmms@gmail.tmt", + "recipientSection": { + "id": "00000000-0000-4000-8000-000000000201" + }, + "feedbackQuestion": { + "id": "00000000-0000-4000-8000-000000000801", + "questionDetails": { + "shouldAllowRichText": true, + "questionType": "TEXT", + "questionText": "Rate 5 other students' products" + } + } + } + }, + "feedbackResponseComments": {}, + "notifications": { + "notification1": { + "id": "00000000-0000-4000-8000-000000000002", + "startTime": "2011-01-01T00:00:00Z", + "endTime": "2099-01-01T00:00:00Z", + "style": "DANGER", + "targetUser": "GENERAL", + "title": "A deprecation note", + "message": "

Deprecation happens in three minutes

", + "shown": false + } + }, + "readNotifications": {} +} diff --git a/src/e2e/resources/data/InstructorStudentListPageE2ETestSql.json b/src/e2e/resources/data/InstructorStudentListPageE2ETestSql.json new file mode 100644 index 000000000000..41c5f4af82e0 --- /dev/null +++ b/src/e2e/resources/data/InstructorStudentListPageE2ETestSql.json @@ -0,0 +1,282 @@ +{ + "accounts": { + "instructorOfCourse1": { + "googleId": "tm.e2e.ISList.instr1", + "name": "Instructor of Course 1", + "email": "ISList.instr1@gmail.tmt", + "id": "00000000-0000-4000-8000-000000000001" + }, + "Student3Course3": { + "googleId": "tm.e2e.ISList.charlie.tmms", + "name": "Charlie D", + "email": "ISList.charlie.tmms@gmail.tmt", + "id": "00000000-0000-4000-8000-000000000002" + } + }, + "accountRequests": {}, + "courses": { + "course1": { + "id": "tm.e2e.ISList.course1", + "name": "Name of Course 1", + "timeZone": "UTC", + "institute": "TEAMMATES Test Institute 1" + }, + "course2": { + "id": "tm.e2e.ISList.course2", + "name": "Name of Course 2", + "timeZone": "UTC", + "institute": "TEAMMATES Test Institute 1" + }, + "course3": { + "id": "tm.e2e.ISList.course3", + "name": "Name of Course 3", + "timeZone": "UTC", + "institute": "TEAMMATES Test Institute 5" + } + }, + "sections": { + "tm.e2e.ISList.course2-SectionA": { + "id": "00000000-0000-4000-8000-000000000201", + "course": { + "id": "tm.e2e.ISList.course2" + }, + "name": "Section A" + }, + "tm.e2e.ISList.course2-SectionB": { + "id": "00000000-0000-4000-8000-000000000202", + "course": { + "id": "tm.e2e.ISList.course2" + }, + "name": "Section B" + }, + "tm.e2e.ISList.course3-Section1": { + "id": "00000000-0000-4000-8000-000000000203", + "course": { + "id": "tm.e2e.ISList.course3" + }, + "name": "Section 1" + } + }, + "teams": { + "tm.e2e.ISList.course2-SectionA-Team1": { + "id": "00000000-0000-4000-8000-000000000301", + "section": { + "id": "00000000-0000-4000-8000-000000000201" + }, + "name": "Team 1" + }, + "tm.e2e.ISList.course2-SectionB-Team2": { + "id": "00000000-0000-4000-8000-000000000302", + "section": { + "id": "00000000-0000-4000-8000-000000000202" + }, + "name": "Team 2" + }, + "tm.e2e.ISList.course3-Section1-Team1": { + "id": "00000000-0000-4000-8000-000000000303", + "section": { + "id": "00000000-0000-4000-8000-000000000203" + }, + "name": "Team 1" + }, + "tm.e2e.ISList.course3-Section1-Team2": { + "id": "00000000-0000-4000-8000-000000000304", + "section": { + "id": "00000000-0000-4000-8000-000000000203" + }, + "name": "Team 2" + } + }, + "deadlineExtensions": {}, + "instructors": { + "instructorOfCourse1": { + "isDisplayedToStudents": true, + "displayName": "Instructor", + "role": "INSTRUCTOR_PERMISSION_ROLE_COOWNER", + "privileges": { + "courseLevel": { + "canModifyCourse": true, + "canModifyInstructor": true, + "canModifySession": true, + "canModifyStudent": true, + "canViewStudentInSections": true, + "canViewSessionInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "id": "00000000-0000-4000-8000-000000000501", + "courseId": "tm.e2e.ISList.course1", + "course": { + "id": "tm.e2e.ISList.course1" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "name": "Instructor1 Course1", + "email": "ISList.instr1@gmail.tmt" + }, + "instructorOfCourse2": { + "isDisplayedToStudents": true, + "displayName": "Instructor", + "role": "INSTRUCTOR_PERMISSION_ROLE_CUSTOM", + "privileges": { + "courseLevel": { + "canModifyCourse": false, + "canModifyInstructor": false, + "canModifySession": false, + "canModifyStudent": false, + "canViewStudentInSections": false, + "canViewSessionInSections": false, + "canSubmitSessionInSections": false, + "canModifySessionCommentsInSections": false + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "id": "00000000-0000-4000-8000-000000000502", + "courseId": "tm.e2e.ISList.course2", + "course": { + "id": "tm.e2e.ISList.course2" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "name": "Instructor1 Course2", + "email": "ISList.instr1@gmail.tmt" + }, + "instructorOfCourse3": { + "isDisplayedToStudents": true, + "displayName": "Instructor", + "role": "INSTRUCTOR_PERMISSION_ROLE_COOWNER", + "privileges": { + "courseLevel": { + "canModifyCourse": true, + "canModifyInstructor": true, + "canModifySession": true, + "canModifyStudent": true, + "canViewStudentInSections": true, + "canViewSessionInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "id": "00000000-0000-4000-8000-000000000503", + "courseId": "tm.e2e.ISList.course3", + "course": { + "id": "tm.e2e.ISList.course3" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "name": "Instructor1 Course3", + "email": "ISList.instr1@gmail.tmt" + } + }, + "students": { + "Student1Course2": { + "comments": "This student's name is Alice", + "id": "00000000-0000-4000-8000-000000000601", + "courseId": "tm.e2e.ISList.course2", + "course": { + "id": "tm.e2e.ISList.course2" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000301" + }, + "name": "Alice", + "email": "ISList.alice.tmms@gmail.tmt" + }, + "Student2Course2": { + "comments": "This student's name is Benny Charles", + "id": "00000000-0000-4000-8000-000000000602", + "courseId": "tm.e2e.ISList.course2", + "course": { + "id": "tm.e2e.ISList.course2" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000302" + }, + "name": "Benny Charles", + "email": "benny.c.tmms@gmail.tmt" + }, + "Student3Course2": { + "comments": "This student's name is Hugh Ivanov", + "id": "00000000-0000-4000-8000-000000000603", + "courseId": "tm.e2e.ISList.course2", + "course": { + "id": "tm.e2e.ISList.course2" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000302" + }, + "name": "Hugh Ivanov", + "email": "hugh.i.tmms@gmail.tmt" + }, + "Student1Course3": { + "comments": "This student's name is Alice", + "id": "00000000-0000-4000-8000-000000000604", + "courseId": "tm.e2e.ISList.course3", + "course": { + "id": "tm.e2e.ISList.course3" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000303" + }, + "name": "Alice Betsy", + "email": "ISList.alice.tmms@gmail.tmt" + }, + "Student2Course3": { + "comments": "This student's name is Benny Charles", + "id": "00000000-0000-4000-8000-000000000605", + "courseId": "tm.e2e.ISList.course3", + "course": { + "id": "tm.e2e.ISList.course3" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000303" + }, + "name": "Benny Charles", + "email": "benny.c.tmms@gmail.tmt" + }, + "Student3Course3": { + "comments": "This student's name is Charlie Davis", + "id": "00000000-0000-4000-8000-000000000606", + "courseId": "tm.e2e.ISList.course3", + "course": { + "id": "tm.e2e.ISList.course3" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000002" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000304" + }, + "name": "Charlie D", + "email": "ISList.charlie.tmms@gmail.tmt" + }, + "Student4Course3": { + "comments": "This student's name is Denny Charlés", + "id": "00000000-0000-4000-8000-000000000607", + "courseId": "tm.e2e.ISList.course3", + "course": { + "id": "tm.e2e.ISList.course3" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000304" + }, + "name": "Denny Charlés", + "email": "denny.c.tmms@gmail.tmt" + } + }, + "feedbackSessions": {}, + "feedbackQuestions": {}, + "feedbackResponses": {}, + "feedbackResponseComments": {}, + "notifications": {}, + "readNotifications": {} +} diff --git a/src/e2e/resources/data/InstructorStudentRecordsPageE2ETestSql.json b/src/e2e/resources/data/InstructorStudentRecordsPageE2ETestSql.json new file mode 100644 index 000000000000..0c5783361fe0 --- /dev/null +++ b/src/e2e/resources/data/InstructorStudentRecordsPageE2ETestSql.json @@ -0,0 +1,99 @@ +{ + "accounts": { + "teammates.test": { + "googleId": "tm.e2e.ISRecords.teammates.test", + "name": "Teammates Test", + "email": "teammates.test@gmail.tmt", + "id": "00000000-0000-4000-8000-000000000001" + }, + "benny.c.tmms@ISR.CS2104": { + "googleId": "tm.e2e.ISRecords.benny.c.tmms", + "name": "Benny Charlés", + "email": "benny.c.tmms@gmail.tmt", + "id": "00000000-0000-4000-8000-000000000002" + } + }, + "accountRequests": {}, + "courses": { + "CS2104": { + "id": "tm.e2e.ISRecords.CS2104", + "name": "Programming Language Concepts", + "timeZone": "UTC", + "institute": "TEAMMATES Test Institute 1" + } + }, + "sections": { + "tm.e2e.ISRecords.CS2104-None": { + "id": "00000000-0000-4000-8000-000000000201", + "course": { + "id": "tm.e2e.ISRecords.CS2104" + }, + "name": "None" + } + }, + "teams": { + "tm.e2e.ISRecords.CS2104-None-Team1": { + "id": "00000000-0000-4000-8000-000000000301", + "section": { + "id": "00000000-0000-4000-8000-000000000201" + }, + "name": "Team 1" + } + }, + "deadlineExtensions": {}, + "instructors": { + "teammates.test.CS2104": { + "isDisplayedToStudents": true, + "displayName": "Instructor", + "role": "INSTRUCTOR_PERMISSION_ROLE_COOWNER", + "privileges": { + "courseLevel": { + "canModifyCourse": true, + "canModifyInstructor": true, + "canModifySession": true, + "canModifyStudent": true, + "canViewStudentInSections": true, + "canViewSessionInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "id": "00000000-0000-4000-8000-000000000501", + "courseId": "tm.e2e.ISRecords.CS2104", + "course": { + "id": "tm.e2e.ISRecords.CS2104" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "name": "Teammates Test", + "email": "teammates.test@gmail.tmt" + } + }, + "students": { + "benny.c.tmms@ISR.CS2104": { + "comments": "This student's name is Benny Charlés", + "id": "00000000-0000-4000-8000-000000000601", + "courseId": "tm.e2e.ISRecords.CS2104", + "course": { + "id": "tm.e2e.ISRecords.CS2104" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000002" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000301" + }, + "name": "Benny Charlés", + "email": "benny.c.tmms@gmail.tmt" + } + }, + "feedbackSessions": {}, + "feedbackQuestions": {}, + "feedbackResponses": {}, + "feedbackResponseComments": {}, + "notifications": {}, + "readNotifications": {} +} diff --git a/src/e2e/resources/data/StudentCourseDetailsPageE2ETestSql.json b/src/e2e/resources/data/StudentCourseDetailsPageE2ETestSql.json new file mode 100644 index 000000000000..c56b3fde3f31 --- /dev/null +++ b/src/e2e/resources/data/StudentCourseDetailsPageE2ETestSql.json @@ -0,0 +1,152 @@ +{ + "accounts": { + "SCDet.instr": { + "googleId": "tm.e2e.SCDet.instr", + "name": "Instructor", + "email": "tmms.test@gmail.tmt", + "id": "00000000-0000-4000-8000-000000000001" + }, + "SCDet.alice": { + "googleId": "tm.e2e.SCDet.alice", + "email": "SCDet.alice@gmail.tmt", + "name": "Alice Betsy", + "id": "00000000-0000-4000-8000-000000000002" + } + }, + "accountRequests": {}, + "courses": { + "SCDet.CS2104": { + "id": "tm.e2e.SCDet.CS2104", + "name": "Programming Language Concepts", + "timeZone": "UTC", + "institute": "TEAMMATES Test Institute 1", + "createdAt": "2012-04-02T11:00:00Z" + } + }, + "sections": { + "tm.e2e.SCDet.CS2104-None": { + "id": "00000000-0000-4000-8000-000000000201", + "course": { + "id": "tm.e2e.SCDet.CS2104" + }, + "name": "None" + } + }, + "teams": { + "tm.e2e.SCDet.CS2104-None-Team1": { + "id": "00000000-0000-4000-8000-000000000301", + "section": { + "id": "00000000-0000-4000-8000-000000000201" + }, + "name": "Team 1" + } + }, + "deadlineExtensions": {}, + "instructors": { + "SCDet.instr": { + "isDisplayedToStudents": true, + "displayName": "Instructor", + "role": "INSTRUCTOR_PERMISSION_ROLE_COOWNER", + "privileges": { + "courseLevel": { + "canModifyCourse": true, + "canModifyInstructor": true, + "canModifySession": true, + "canModifyStudent": true, + "canViewStudentInSections": true, + "canViewSessionInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "id": "00000000-0000-4000-8000-000000000501", + "courseId": "tm.e2e.SCDet.CS2104", + "course": { + "id": "tm.e2e.SCDet.CS2104" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "name": "Teammates Test", + "email": "SCDet.instr@gmail.tmt" + }, + "SCDet.instr2": { + "isDisplayedToStudents": true, + "displayName": "Instructor", + "role": "INSTRUCTOR_PERMISSION_ROLE_COOWNER", + "privileges": { + "courseLevel": { + "canModifyCourse": true, + "canModifyInstructor": true, + "canModifySession": true, + "canModifyStudent": true, + "canViewStudentInSections": true, + "canViewSessionInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "id": "00000000-0000-4000-8000-000000000502", + "courseId": "tm.e2e.SCDet.CS2104", + "course": { + "id": "tm.e2e.SCDet.CS2104" + }, + "name": "Teammates Test 2", + "email": "SCDet.instr2@gmail.tmt" + } + }, + "students": { + "SCDet.alice": { + "comments": "This student's name is Alice Betsy", + "id": "00000000-0000-4000-8000-000000000601", + "courseId": "tm.e2e.SCDet.CS2104", + "course": { + "id": "tm.e2e.SCDet.CS2104" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000301" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000002" + }, + "name": "Alice Betsy", + "email": "SCDet.alice@gmail.tmt" + }, + "SCDet.benny": { + "comments": "This student's name is Benny Charles", + "id": "00000000-0000-4000-8000-000000000602", + "courseId": "tm.e2e.SCDet.CS2104", + "course": { + "id": "tm.e2e.SCDet.CS2104" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000301" + }, + "name": "Benny Charles", + "email": "SCDet.benny@gmail.tmt" + }, + "SCDet.charlie": { + "comments": "This student's name is Charlie Davis", + "id": "00000000-0000-4000-8000-000000000603", + "courseId": "tm.e2e.SCDet.CS2104", + "course": { + "id": "tm.e2e.SCDet.CS2104" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000301" + }, + "name": "Charlie Davis", + "email": "SCDet.charlie@gmail.tmt" + } + }, + "feedbackSessions": {}, + "feedbackQuestions": {}, + "feedbackResponses": {}, + "feedbackResponseComments": {}, + "notifications": {}, + "readNotifications": {} +} diff --git a/src/e2e/resources/data/StudentCourseJoinConfirmationPageE2ETestSql.json b/src/e2e/resources/data/StudentCourseJoinConfirmationPageE2ETestSql.json new file mode 100644 index 000000000000..4866f6f16c29 --- /dev/null +++ b/src/e2e/resources/data/StudentCourseJoinConfirmationPageE2ETestSql.json @@ -0,0 +1,159 @@ +{ + "accounts": { + "SCJoinConf.instr": { + "googleId": "tm.e2e.SCJoinConf.instr", + "name": "Teammates Test", + "email": "SCJoinConf.instr@gmail.tmt", + "id": "00000000-0000-4000-8000-000000000001" + }, + "alice.tmms": { + "googleId": "tm.e2e.SCJoinConf.alice", + "name": "Alice B", + "email": "SCJoinConf.alice@gmail.tmt", + "id": "00000000-0000-4000-8000-000000000002" + } + }, + "accountRequests": {}, + "courses": { + "SCJoinConf.CS2104": { + "id": "tm.e2e.SCJoinConf.CS2104", + "name": "Programming Language Concepts", + "timeZone": "UTC", + "institute": "TEAMMATES Test Institute 1" + }, + "SCJoinConf.CS1101": { + "id": "tm.e2e.SCJoinConf.CS1101", + "name": "Programming Methodology", + "timeZone": "UTC", + "institute": "TEAMMATES Test Institute 1" + } + }, + "sections": { + "tm.e2e.SCJoinConf.CS2104-None": { + "id": "00000000-0000-4000-8000-000000000201", + "course": { + "id": "tm.e2e.SCJoinConf.CS2104" + }, + "name": "None" + }, + "tm.e2e.SCJoinConf.CS1101-None": { + "id": "00000000-0000-4000-8000-000000000202", + "course": { + "id": "tm.e2e.SCJoinConf.CS1101" + }, + "name": "None" + } + }, + "teams": { + "tm.e2e.SCJoinConf.CS2104-None-Team1": { + "id": "00000000-0000-4000-8000-000000000301", + "section": { + "id": "00000000-0000-4000-8000-000000000201" + }, + "name": "Team 1" + }, + "tm.e2e.SCJoinConf.CS1101-None-Team1": { + "id": "00000000-0000-4000-8000-000000000302", + "section": { + "id": "00000000-0000-4000-8000-000000000202" + }, + "name": "Team 1" + } + }, + "deadlineExtensions": {}, + "instructors": { + "SCJoinConf.instr.CS2104": { + "isDisplayedToStudents": true, + "displayName": "Co-owner", + "role": "INSTRUCTOR_PERMISSION_ROLE_COOWNER", + "privileges": { + "courseLevel": { + "canModifyCourse": true, + "canModifyInstructor": true, + "canModifySession": true, + "canModifyStudent": true, + "canViewStudentInSections": true, + "canViewSessionInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "id": "00000000-0000-4000-8000-000000000501", + "course": { + "id": "tm.e2e.SCJoinConf.CS2104" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "name": "Teammates Test", + "email": "SCJoinConf.instr@gmail.tmt" + }, + "SCJoinConf.instr.CS1101": { + "isDisplayedToStudents": true, + "displayName": "Co-owner", + "role": "INSTRUCTOR_PERMISSION_ROLE_COOWNER", + "privileges": { + "courseLevel": { + "canModifyCourse": true, + "canModifyInstructor": true, + "canModifySession": true, + "canModifyStudent": true, + "canViewStudentInSections": true, + "canViewSessionInSections": true, + "canSubmitSessionInSections": true, + "canModifySessionCommentsInSections": true + }, + "sectionLevel": {}, + "sessionLevel": {} + }, + "courseId": "tm.e2e.SCJoinConf.CS1101", + "course": { + "id": "tm.e2e.SCJoinConf.CS1101" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000001" + }, + "name": "Teammates Test", + "email": "SCJoinConf.instr@gmail.tmt" + } + }, + "students": { + "alice.tmms@SCJoinConf.CS2104": { + "comments": "This student's name is Amy Betsy", + "id": "00000000-0000-4000-8000-000000000601", + "courseId": "tm.e2e.SCJoinConf.CS2104", + "course": { + "id": "tm.e2e.SCJoinConf.CS2104" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000301" + }, + "name": "Amy Betsy", + "email": "SCJoinConf.alice@gmail.tmt" + }, + "alice.tmms@SCJoinConf.CS1101": { + "comments": "This student's name is Amy Betsy", + "id": "00000000-0000-4000-8000-000000000602", + "courseId": "tm.e2e.SCJoinConf.CS1101", + "course": { + "id": "tm.e2e.SCJoinConf.CS1101" + }, + "account": { + "id": "00000000-0000-4000-8000-000000000002" + }, + "team": { + "id": "00000000-0000-4000-8000-000000000301" + }, + "name": "Amy Betsy", + "email": "SCJoinConf.alice@gmail.tmt" + } + }, + "feedbackSessions": {}, + "feedbackQuestions": {}, + "feedbackResponses": {}, + "feedbackResponseComments": {}, + "notifications": {}, + "readNotifications": {} +} diff --git a/src/e2e/resources/testng-e2e-sql.xml b/src/e2e/resources/testng-e2e-sql.xml index 78dcc2d13a1f..e8615603a8b1 100644 --- a/src/e2e/resources/testng-e2e-sql.xml +++ b/src/e2e/resources/testng-e2e-sql.xml @@ -9,10 +9,19 @@ + + + + + + + + + diff --git a/src/it/java/teammates/it/sqllogic/core/AccountRequestsLogicIT.java b/src/it/java/teammates/it/sqllogic/core/AccountRequestsLogicIT.java index c449f3358a01..c4b128ddbfd4 100644 --- a/src/it/java/teammates/it/sqllogic/core/AccountRequestsLogicIT.java +++ b/src/it/java/teammates/it/sqllogic/core/AccountRequestsLogicIT.java @@ -1,9 +1,11 @@ package teammates.it.sqllogic.core; import java.time.Instant; +import java.util.UUID; import org.testng.annotations.Test; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.exception.EntityAlreadyExistsException; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; @@ -19,6 +21,23 @@ public class AccountRequestsLogicIT extends BaseTestCaseWithSqlDatabaseAccess { private AccountRequestsLogic accountRequestsLogic = AccountRequestsLogic.inst(); + @Test + public void testGetAccountRequest_nonExistentAccountRequest_returnsNull() { + UUID id = UUID.randomUUID(); + AccountRequest actualAccountRequest = accountRequestsLogic.getAccountRequest(id); + assertNull(actualAccountRequest); + } + + @Test + public void testGetAccountRequest_existingAccountRequest_getsSuccessfully() throws InvalidParametersException { + AccountRequest expectedAccountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); + UUID id = expectedAccountRequest.getId(); + accountRequestsLogic.createAccountRequest(expectedAccountRequest); + AccountRequest actualAccountRequest = accountRequestsLogic.getAccountRequest(id); + assertEquals(expectedAccountRequest, actualAccountRequest); + } + @Test public void testResetAccountRequest() throws EntityAlreadyExistsException, InvalidParametersException, EntityDoesNotExistException { @@ -28,31 +47,34 @@ public void testResetAccountRequest() String name = "name lee"; String email = "email@gmail.com"; String institute = "institute"; + AccountRequestStatus status = AccountRequestStatus.PENDING; + String comments = "comments"; - AccountRequest toReset = accountRequestsLogic.createAccountRequest(name, email, institute); + AccountRequest toReset = accountRequestsLogic.createAccountRequest(name, email, institute, status, comments); AccountRequestsDb accountRequestsDb = AccountRequestsDb.inst(); toReset.setRegisteredAt(Instant.now()); - toReset = accountRequestsDb.getAccountRequest(email, institute); + UUID id = toReset.getId(); + toReset = accountRequestsDb.getAccountRequest(id); assertNotNull(toReset); assertNotNull(toReset.getRegisteredAt()); ______TS("success: reset account request that already exists"); - AccountRequest resetted = accountRequestsLogic.resetAccountRequest(email, institute); + AccountRequest resetted = accountRequestsLogic.resetAccountRequest(id); assertNull(resetted.getRegisteredAt()); ______TS("success: test delete account request"); - accountRequestsLogic.deleteAccountRequest(email, institute); + accountRequestsLogic.deleteAccountRequest(toReset.getId()); - assertNull(accountRequestsLogic.getAccountRequest(email, institute)); + assertNull(accountRequestsLogic.getAccountRequest(toReset.getId())); ______TS("failure: reset account request that does not exist"); assertThrows(EntityDoesNotExistException.class, - () -> accountRequestsLogic.resetAccountRequest(name, institute)); + () -> accountRequestsLogic.resetAccountRequest(id)); } } diff --git a/src/it/java/teammates/it/sqllogic/core/DataBundleLogicIT.java b/src/it/java/teammates/it/sqllogic/core/DataBundleLogicIT.java index f85710376321..0e94674fc567 100644 --- a/src/it/java/teammates/it/sqllogic/core/DataBundleLogicIT.java +++ b/src/it/java/teammates/it/sqllogic/core/DataBundleLogicIT.java @@ -8,6 +8,7 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.InstructorPermissionRole; import teammates.common.datatransfer.InstructorPrivileges; @@ -62,7 +63,7 @@ public void testCreateDataBundle_typicalValues_createdCorrectly() throws Excepti AccountRequest actualAccountRequest = dataBundle.accountRequests.get("instructor1"); AccountRequest expectedAccountRequest = new AccountRequest("instr1@teammates.tmt", "Instructor 1", - "TEAMMATES Test Institute 1"); + "TEAMMATES Test Institute 1", AccountRequestStatus.REGISTERED, "These are some comments."); expectedAccountRequest.setId(actualAccountRequest.getId()); expectedAccountRequest.setRegisteredAt(Instant.parse("2015-02-14T00:00:00Z")); expectedAccountRequest.setRegistrationKey(actualAccountRequest.getRegistrationKey()); @@ -202,7 +203,7 @@ public void testCreateDataBundle_typicalValues_createdCorrectly() throws Excepti FeedbackResponseComment expectedComment1 = new FeedbackResponseComment(expectedResponse1, "instr1@teammates.tmt", FeedbackParticipantType.INSTRUCTORS, expectedSection, expectedSection, "Instructor 1 comment to student 1 self feedback", false, false, - new ArrayList(), new ArrayList(), "instr1@teammates.tmt"); + new ArrayList<>(), new ArrayList<>(), "instr1@teammates.tmt"); expectedComment1.setId(actualComment1.getId()); verifyEquals(expectedComment1, actualComment1); } diff --git a/src/it/java/teammates/it/sqllogic/core/FeedbackSessionLogsLogicIT.java b/src/it/java/teammates/it/sqllogic/core/FeedbackSessionLogsLogicIT.java new file mode 100644 index 000000000000..1007cd503284 --- /dev/null +++ b/src/it/java/teammates/it/sqllogic/core/FeedbackSessionLogsLogicIT.java @@ -0,0 +1,131 @@ +package teammates.it.sqllogic.core; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.SqlDataBundle; +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.util.HibernateUtil; +import teammates.it.test.BaseTestCaseWithSqlDatabaseAccess; +import teammates.sqllogic.core.FeedbackSessionLogsLogic; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.storage.sqlentity.Student; + +/** + * SUT: {@link FeedbackSessionLogsLogic}. + */ +public class FeedbackSessionLogsLogicIT extends BaseTestCaseWithSqlDatabaseAccess { + + private FeedbackSessionLogsLogic fslLogic = FeedbackSessionLogsLogic.inst(); + + private SqlDataBundle typicalDataBundle; + + @Override + @BeforeClass + public void setupClass() { + super.setupClass(); + typicalDataBundle = getTypicalSqlDataBundle(); + } + + @Override + @BeforeMethod + protected void setUp() throws Exception { + super.setUp(); + persistDataBundle(typicalDataBundle); + HibernateUtil.flushSession(); + HibernateUtil.clearSession(); + } + + @Test + public void test_createFeedbackSessionLog_success() { + Course course = typicalDataBundle.courses.get("course1"); + FeedbackSession fs = typicalDataBundle.feedbackSessions.get("session1InCourse1"); + Student student = typicalDataBundle.students.get("student1InCourse1"); + Instant timestamp = Instant.now(); + FeedbackSessionLog newLog1 = new FeedbackSessionLog(student, fs, FeedbackSessionLogType.ACCESS, timestamp); + FeedbackSessionLog newLog2 = new FeedbackSessionLog(student, fs, FeedbackSessionLogType.SUBMISSION, timestamp); + FeedbackSessionLog newLog3 = new FeedbackSessionLog(student, fs, FeedbackSessionLogType.VIEW_RESULT, timestamp); + List expected = List.of(newLog1, newLog2, newLog3); + + fslLogic.createFeedbackSessionLogs(expected); + + List actual = fslLogic.getOrderedFeedbackSessionLogs(course.getId(), student.getId(), + fs.getId(), timestamp, timestamp.plusSeconds(1)); + + assertEquals(expected, actual); + } + + @Test + public void test_getOrderedFeedbackSessionLogs_success() { + Instant startTime = Instant.parse("2012-01-01T12:00:00Z"); + Instant endTime = Instant.parse("2012-01-01T23:59:59Z"); + Course course = typicalDataBundle.courses.get("course1"); + Student student1 = typicalDataBundle.students.get("student1InCourse1"); + FeedbackSession fs1 = typicalDataBundle.feedbackSessions.get("session1InCourse1"); + + FeedbackSessionLog student1Session1Log1 = typicalDataBundle.feedbackSessionLogs.get("student1Session1Log1"); + FeedbackSessionLog student1Session2Log1 = typicalDataBundle.feedbackSessionLogs.get("student1Session2Log1"); + FeedbackSessionLog student1Session2Log2 = typicalDataBundle.feedbackSessionLogs.get("student1Session2Log2"); + FeedbackSessionLog student2Session1Log1 = typicalDataBundle.feedbackSessionLogs.get("student2Session1Log1"); + FeedbackSessionLog student2Session1Log2 = typicalDataBundle.feedbackSessionLogs.get("student2Session1Log2"); + + ______TS("Return logs belonging to a course in time range"); + List expectedLogs = List.of( + student1Session1Log1, + student1Session2Log1, + student1Session2Log2, + student2Session1Log1, + student2Session1Log2); + + List actualLogs = fslLogic.getOrderedFeedbackSessionLogs(course.getId(), null, null, + startTime, endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("Return logs belonging to a student in a course in time range"); + expectedLogs = List.of( + student1Session1Log1, + student1Session2Log1, + student1Session2Log2); + + actualLogs = fslLogic.getOrderedFeedbackSessionLogs(course.getId(), student1.getId(), null, startTime, + endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("Return logs belonging to a feedback session in time range"); + expectedLogs = List.of( + student1Session1Log1, + student2Session1Log1, + student2Session1Log2); + + actualLogs = fslLogic.getOrderedFeedbackSessionLogs(course.getId(), null, fs1.getId(), startTime, endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("Return logs belonging to a student in a feedback session in time range"); + expectedLogs = List.of(student1Session1Log1); + + actualLogs = fslLogic.getOrderedFeedbackSessionLogs(course.getId(), student1.getId(), fs1.getId(), + startTime, + endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("No logs in time range, return empty list"); + expectedLogs = new ArrayList<>(); + + actualLogs = fslLogic.getOrderedFeedbackSessionLogs(course.getId(), null, null, endTime.plusSeconds(3600), + endTime.plusSeconds(7200)); + + assertEquals(expectedLogs, actualLogs); + } + +} diff --git a/src/it/java/teammates/it/storage/sqlapi/AccountRequestsDbIT.java b/src/it/java/teammates/it/storage/sqlapi/AccountRequestsDbIT.java index 8af4c8065df5..6807e43a9b42 100644 --- a/src/it/java/teammates/it/storage/sqlapi/AccountRequestsDbIT.java +++ b/src/it/java/teammates/it/storage/sqlapi/AccountRequestsDbIT.java @@ -1,11 +1,13 @@ package teammates.it.storage.sqlapi; import java.util.List; +import java.util.UUID; import org.testng.annotations.Test; -import teammates.common.exception.EntityAlreadyExistsException; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.exception.EntityDoesNotExistException; +import teammates.common.exception.InvalidParametersException; import teammates.it.test.BaseTestCaseWithSqlDatabaseAccess; import teammates.storage.sqlapi.AccountRequestsDb; import teammates.storage.sqlentity.AccountRequest; @@ -21,13 +23,13 @@ public class AccountRequestsDbIT extends BaseTestCaseWithSqlDatabaseAccess { public void testCreateReadDeleteAccountRequest() throws Exception { ______TS("Create account request, does not exists, succeeds"); - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); accountRequestDb.createAccountRequest(accountRequest); - ______TS("Read account request using the given email and institute"); + ______TS("Read account request using the given ID"); - AccountRequest actualAccReqEmalAndInstitute = - accountRequestDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + AccountRequest actualAccReqEmalAndInstitute = accountRequestDb.getAccountRequest(accountRequest.getId()); verifyEquals(accountRequest, actualAccReqEmalAndInstitute); ______TS("Read account request using the given registration key"); @@ -51,29 +53,49 @@ public void testCreateReadDeleteAccountRequest() throws Exception { accountRequest.getCreatedAt().minusMillis(2000)); assertEquals(0, actualAccReqCreatedAtOutside.size()); - ______TS("Create acccount request, already exists, execption thrown"); + ______TS("Create account request, same email address and institute already exist, creates successfully"); AccountRequest identicalAccountRequest = - new AccountRequest("test@gmail.com", "name", "institute"); + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); assertNotSame(accountRequest, identicalAccountRequest); - assertThrows(EntityAlreadyExistsException.class, - () -> accountRequestDb.createAccountRequest(identicalAccountRequest)); + accountRequestDb.createAccountRequest(identicalAccountRequest); + AccountRequest actualIdenticalAccountRequest = + accountRequestDb.getAccountRequestByRegistrationKey(identicalAccountRequest.getRegistrationKey()); + verifyEquals(identicalAccountRequest, actualIdenticalAccountRequest); ______TS("Delete account request that was created"); accountRequestDb.deleteAccountRequest(accountRequest); AccountRequest actualAccountRequest = - accountRequestDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + accountRequestDb.getAccountRequestByRegistrationKey(accountRequest.getRegistrationKey()); assertNull(actualAccountRequest); } + @Test + public void testGetAccountRequest_nonExistentAccountRequest_returnsNull() { + UUID id = UUID.randomUUID(); + AccountRequest actualAccountRequest = accountRequestDb.getAccountRequest(id); + assertNull(actualAccountRequest); + } + + @Test + public void testGetAccountRequest_existingAccountRequest_getsSuccessfully() throws InvalidParametersException { + AccountRequest expectedAccountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); + UUID id = expectedAccountRequest.getId(); + accountRequestDb.createAccountRequest(expectedAccountRequest); + AccountRequest actualAccountRequest = accountRequestDb.getAccountRequest(id); + assertEquals(expectedAccountRequest, actualAccountRequest); + } + @Test public void testUpdateAccountRequest() throws Exception { ______TS("Update account request, does not exists, exception thrown"); - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); assertThrows(EntityDoesNotExistException.class, () -> accountRequestDb.updateAccountRequest(accountRequest)); @@ -84,8 +106,7 @@ public void testUpdateAccountRequest() throws Exception { accountRequest.setName("new account request name"); accountRequestDb.updateAccountRequest(accountRequest); - AccountRequest actual = accountRequestDb.getAccountRequest( - accountRequest.getEmail(), accountRequest.getInstitute()); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); verifyEquals(accountRequest, actual); } @@ -95,11 +116,12 @@ public void testSqlInjectionInCreateAccountRequestEmailField() throws Exception // Attempt to use SQL commands in email field String email = "email'/**/OR/**/1=1/**/@gmail.com"; - AccountRequest accountRequest = new AccountRequest(email, "name", "institute"); + AccountRequest accountRequest = + new AccountRequest(email, "name", "institute", AccountRequestStatus.PENDING, "comments"); // The system should treat the input as a plain text string accountRequestDb.createAccountRequest(accountRequest); - AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); assertEquals(email, actual.getEmail()); } @@ -109,11 +131,12 @@ public void testSqlInjectionInCreateAccountRequestNameField() throws Exception { // Attempt to use SQL commands in name field String name = "name'; SELECT * FROM account_requests; --"; - AccountRequest accountRequest = new AccountRequest("test@gmail.com", name, "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", name, "institute", AccountRequestStatus.PENDING, "comments"); // The system should treat the input as a plain text string accountRequestDb.createAccountRequest(accountRequest); - AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); assertEquals(name, actual.getName()); } @@ -123,34 +146,36 @@ public void testSqlInjectionInCreateAccountRequestInstituteField() throws Except // Attempt to use SQL commands in institute field String institute = "institute'; DROP TABLE account_requests; --"; - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", institute); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", institute, AccountRequestStatus.PENDING, "comments"); // The system should treat the input as a plain text string accountRequestDb.createAccountRequest(accountRequest); - AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getEmail(), institute); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); assertEquals(institute, actual.getInstitute()); } @Test - public void testSqlInjectionInGetAccountRequest() throws Exception { - ______TS("SQL Injection test in getAccountRequest"); - - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); - accountRequestDb.createAccountRequest(accountRequest); + public void testSqlInjectionInCreateAccountRequestCommentsField() throws Exception { + ______TS("SQL Injection test in comments field"); - String instituteInjection = "institute'; DROP TABLE account_requests; --"; - AccountRequest actualInjection = accountRequestDb.getAccountRequest(accountRequest.getEmail(), instituteInjection); - assertNull(actualInjection); + // Attempt to use SQL commands in comments field + String comments = "comment'; DROP TABLE account_requests; --"; + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, comments); - AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); - assertEquals(accountRequest, actual); + // The system should treat the input as a plain text string + accountRequestDb.createAccountRequest(accountRequest); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); + assertEquals(comments, actual.getComments()); } @Test public void testSqlInjectionInGetAccountRequestByRegistrationKey() throws Exception { ______TS("SQL Injection test in getAccountRequestByRegistrationKey"); - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); accountRequestDb.createAccountRequest(accountRequest); String regKeyInjection = "regKey'; DROP TABLE account_requests; --"; @@ -161,18 +186,35 @@ public void testSqlInjectionInGetAccountRequestByRegistrationKey() throws Except assertEquals(accountRequest, actual); } + @Test + public void testSqlInjectionInGetApprovedAccountRequestsForEmail() throws Exception { + ______TS("SQL Injection test in getApprovedAccountRequestsForEmail"); + + String email = "test@gmail.com"; + AccountRequest accountRequest = + new AccountRequest(email, "name", "institute", AccountRequestStatus.APPROVED, "comments"); + accountRequestDb.createAccountRequest(accountRequest); + + // Attempt to use SQL commands in email field + String emailInjection = "email'/**/OR/**/1=1/**/@gmail.com"; + List actualInjection = accountRequestDb.getApprovedAccountRequestsForEmail(emailInjection); + // The system should treat the input as a plain text string + assertEquals(0, actualInjection.size()); + } + @Test public void testSqlInjectionInUpdateAccountRequest() throws Exception { ______TS("SQL Injection test in updateAccountRequest"); - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); accountRequestDb.createAccountRequest(accountRequest); String nameInjection = "newName'; DROP TABLE account_requests; --"; accountRequest.setName(nameInjection); accountRequestDb.updateAccountRequest(accountRequest); - AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); assertEquals(accountRequest, actual); } @@ -180,16 +222,18 @@ public void testSqlInjectionInUpdateAccountRequest() throws Exception { public void testSqlInjectionInDeleteAccountRequest() throws Exception { ______TS("SQL Injection test in deleteAccountRequest"); - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); accountRequestDb.createAccountRequest(accountRequest); String emailInjection = "email'/**/OR/**/1=1/**/@gmail.com"; String nameInjection = "name'; DROP TABLE account_requests; --"; String instituteInjection = "institute'; DROP TABLE account_requests; --"; - AccountRequest accountRequestInjection = new AccountRequest(emailInjection, nameInjection, instituteInjection); + AccountRequest accountRequestInjection = new AccountRequest(emailInjection, nameInjection, instituteInjection, + AccountRequestStatus.PENDING, "comments"); accountRequestDb.deleteAccountRequest(accountRequestInjection); - AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); assertEquals(accountRequest, actual); } @@ -197,14 +241,15 @@ public void testSqlInjectionInDeleteAccountRequest() throws Exception { public void testSqlInjectionSearchAccountRequestsInWholeSystem() throws Exception { ______TS("SQL Injection test in searchAccountRequestsInWholeSystem"); - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); accountRequestDb.createAccountRequest(accountRequest); String searchInjection = "institute'; DROP TABLE account_requests; --"; List actualInjection = accountRequestDb.searchAccountRequestsInWholeSystem(searchInjection); assertEquals(0, actualInjection.size()); - AccountRequest actual = accountRequestDb.getAccountRequest("test@gmail.com", "institute"); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); assertEquals(accountRequest, actual); } } diff --git a/src/it/java/teammates/it/storage/sqlapi/FeedbackResponseCommentsDbIT.java b/src/it/java/teammates/it/storage/sqlapi/FeedbackResponseCommentsDbIT.java index f4a6bcc7c875..040e30419c27 100644 --- a/src/it/java/teammates/it/storage/sqlapi/FeedbackResponseCommentsDbIT.java +++ b/src/it/java/teammates/it/storage/sqlapi/FeedbackResponseCommentsDbIT.java @@ -101,7 +101,7 @@ public void testSqlInjectionInCreateFeedbackResponseComment() throws Exception { FeedbackResponseComment newFrc = new FeedbackResponseComment( fr, "", FeedbackParticipantType.INSTRUCTORS, s, s, "", false, false, - new ArrayList(), new ArrayList(), sqli); + new ArrayList<>(), new ArrayList<>(), sqli); frcDb.createFeedbackResponseComment(newFrc); diff --git a/src/it/java/teammates/it/storage/sqlapi/FeedbackSessionLogsDbIT.java b/src/it/java/teammates/it/storage/sqlapi/FeedbackSessionLogsDbIT.java new file mode 100644 index 000000000000..30b5276893b0 --- /dev/null +++ b/src/it/java/teammates/it/storage/sqlapi/FeedbackSessionLogsDbIT.java @@ -0,0 +1,128 @@ +package teammates.it.storage.sqlapi; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.SqlDataBundle; +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.util.HibernateUtil; +import teammates.it.test.BaseTestCaseWithSqlDatabaseAccess; +import teammates.storage.sqlapi.FeedbackSessionLogsDb; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.storage.sqlentity.Student; + +/** + * SUT: {@link FeedbackSessionLogsDb}. + */ +public class FeedbackSessionLogsDbIT extends BaseTestCaseWithSqlDatabaseAccess { + + private final FeedbackSessionLogsDb fslDb = FeedbackSessionLogsDb.inst(); + + private SqlDataBundle typicalDataBundle; + + @Override + @BeforeClass + public void setupClass() { + super.setupClass(); + typicalDataBundle = getTypicalSqlDataBundle(); + } + + @Override + @BeforeMethod + protected void setUp() throws Exception { + super.setUp(); + persistDataBundle(typicalDataBundle); + HibernateUtil.flushSession(); + } + + @Test + public void test_createFeedbackSessionLog_success() { + Course course = typicalDataBundle.courses.get("course1"); + FeedbackSession feedbackSession = typicalDataBundle.feedbackSessions.get("session1InCourse1"); + Student student = typicalDataBundle.students.get("student1InCourse1"); + + Instant logTimestamp = Instant.parse("2011-01-01T00:00:00Z"); + FeedbackSessionLog expected = new FeedbackSessionLog(student, feedbackSession, FeedbackSessionLogType.ACCESS, + logTimestamp); + + fslDb.createFeedbackSessionLog(expected); + + List actualLogs = fslDb.getOrderedFeedbackSessionLogs(course.getId(), student.getId(), + feedbackSession.getId(), logTimestamp, logTimestamp.plusSeconds(1)); + + assertEquals(actualLogs.size(), 1); + assertEquals(expected, actualLogs.get(0)); + } + + @Test + public void test_getOrderedFeedbackSessionLogs_success() { + Instant startTime = Instant.parse("2012-01-01T12:00:00Z"); + Instant endTime = Instant.parse("2012-01-01T23:59:59Z"); + Course course = typicalDataBundle.courses.get("course1"); + Student student1 = typicalDataBundle.students.get("student1InCourse1"); + FeedbackSession fs1 = typicalDataBundle.feedbackSessions.get("session1InCourse1"); + + FeedbackSessionLog student1Session1Log1 = typicalDataBundle.feedbackSessionLogs.get("student1Session1Log1"); + FeedbackSessionLog student1Session2Log1 = typicalDataBundle.feedbackSessionLogs.get("student1Session2Log1"); + FeedbackSessionLog student1Session2Log2 = typicalDataBundle.feedbackSessionLogs.get("student1Session2Log2"); + FeedbackSessionLog student2Session1Log1 = typicalDataBundle.feedbackSessionLogs.get("student2Session1Log1"); + FeedbackSessionLog student2Session1Log2 = typicalDataBundle.feedbackSessionLogs.get("student2Session1Log2"); + + ______TS("Return logs belonging to a course in time range"); + List expectedLogs = List.of( + student1Session1Log1, + student1Session2Log1, + student1Session2Log2, + student2Session1Log1, + student2Session1Log2 + ); + + List actualLogs = fslDb.getOrderedFeedbackSessionLogs(course.getId(), null, null, + startTime, endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("Return logs belonging to a student in time range"); + expectedLogs = List.of( + student1Session1Log1, + student1Session2Log1, + student1Session2Log2); + + actualLogs = fslDb.getOrderedFeedbackSessionLogs(course.getId(), student1.getId(), null, startTime, endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("Return logs belonging to a feedback session in time range"); + expectedLogs = List.of( + student1Session1Log1, + student2Session1Log1, + student2Session1Log2); + + actualLogs = fslDb.getOrderedFeedbackSessionLogs(course.getId(), null, fs1.getId(), startTime, endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("Return logs belonging to a student in a feedback session in time range"); + expectedLogs = List.of(student1Session1Log1); + + actualLogs = fslDb.getOrderedFeedbackSessionLogs(course.getId(), student1.getId(), fs1.getId(), startTime, + endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("No logs in time range, return empty list"); + expectedLogs = new ArrayList<>(); + + actualLogs = fslDb.getOrderedFeedbackSessionLogs(course.getId(), null, null, endTime.plusSeconds(3600), + endTime.plusSeconds(7200)); + + assertEquals(expectedLogs, actualLogs); + } +} diff --git a/src/it/java/teammates/it/storage/sqlsearch/AccountRequestSearchIT.java b/src/it/java/teammates/it/storage/sqlsearch/AccountRequestSearchIT.java index db64c17c2ab8..a9b196eafc85 100644 --- a/src/it/java/teammates/it/storage/sqlsearch/AccountRequestSearchIT.java +++ b/src/it/java/teammates/it/storage/sqlsearch/AccountRequestSearchIT.java @@ -89,6 +89,16 @@ public void allTests() throws Exception { results = accountRequestsDb.searchAccountRequestsInWholeSystem("\"TEAMMATES Test Institute 2\""); verifySearchResults(results, unregisteredInstructor2); + ______TS("success: search for account requests; account requests should be searchable by their comments"); + + results = accountRequestsDb.searchAccountRequestsInWholeSystem("Comments for account request from instructor2"); + verifySearchResults(results, ins2General); + + ______TS("success: search for account requests; account requests should be searchable by their status"); + + results = accountRequestsDb.searchAccountRequestsInWholeSystem("registered"); + verifySearchResults(results, ins2General); + ______TS("success: search for account requests; unregistered account requests should be searchable"); results = accountRequestsDb.searchAccountRequestsInWholeSystem("\"unregisteredinstructor1@gmail.tmt\""); diff --git a/src/it/java/teammates/it/test/BaseTestCaseWithSqlDatabaseAccess.java b/src/it/java/teammates/it/test/BaseTestCaseWithSqlDatabaseAccess.java index f6f5adc72f5d..83e1b4399c4f 100644 --- a/src/it/java/teammates/it/test/BaseTestCaseWithSqlDatabaseAccess.java +++ b/src/it/java/teammates/it/test/BaseTestCaseWithSqlDatabaseAccess.java @@ -257,7 +257,7 @@ private BaseEntity getEntity(BaseEntity entity) { return logic.getNotification(((Notification) entity).getId()); } else if (entity instanceof AccountRequest) { AccountRequest accountRequest = (AccountRequest) entity; - return logic.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + return logic.getAccountRequest(accountRequest.getId()); } else if (entity instanceof Instructor) { return logic.getInstructor(((Instructor) entity).getId()); } else if (entity instanceof Student) { diff --git a/src/it/java/teammates/it/test/TestProperties.java b/src/it/java/teammates/it/test/TestProperties.java index 55e9f4c77e95..8ab06ea2a5b2 100644 --- a/src/it/java/teammates/it/test/TestProperties.java +++ b/src/it/java/teammates/it/test/TestProperties.java @@ -9,6 +9,7 @@ /** * Settings for integration tests. */ +@SuppressWarnings("PMD.TestClassWithoutTestCases") public final class TestProperties { /** The directory where JSON files used to create data bundles are stored. */ diff --git a/src/it/java/teammates/it/ui/webapi/AccountRequestSearchIndexingWorkerActionIT.java b/src/it/java/teammates/it/ui/webapi/AccountRequestSearchIndexingWorkerActionIT.java index a90fb7c9421c..6ae1e9e51c45 100644 --- a/src/it/java/teammates/it/ui/webapi/AccountRequestSearchIndexingWorkerActionIT.java +++ b/src/it/java/teammates/it/ui/webapi/AccountRequestSearchIndexingWorkerActionIT.java @@ -1,6 +1,7 @@ package teammates.it.ui.webapi; import java.util.List; +import java.util.UUID; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -46,6 +47,7 @@ public void testExecute() throws Exception { } AccountRequest accountRequest = typicalBundle.accountRequests.get("instructor1"); + UUID accountRequestId = accountRequest.getId(); ______TS("account request not yet indexed should not be searchable"); @@ -56,8 +58,7 @@ public void testExecute() throws Exception { ______TS("account request indexed should be searchable"); String[] submissionParams = new String[] { - ParamsNames.INSTRUCTOR_EMAIL, accountRequest.getEmail(), - ParamsNames.INSTRUCTOR_INSTITUTION, accountRequest.getInstitute(), + ParamsNames.ACCOUNT_REQUEST_ID, accountRequestId.toString(), }; AccountRequestSearchIndexingWorkerAction action = getAction(submissionParams); diff --git a/src/it/java/teammates/it/ui/webapi/BaseActionIT.java b/src/it/java/teammates/it/ui/webapi/BaseActionIT.java index 950de8655e73..b46189f7a84d 100644 --- a/src/it/java/teammates/it/ui/webapi/BaseActionIT.java +++ b/src/it/java/teammates/it/ui/webapi/BaseActionIT.java @@ -5,7 +5,7 @@ import java.util.List; import java.util.Map; -import javax.servlet.http.Cookie; +import jakarta.servlet.http.Cookie; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpDelete; @@ -22,6 +22,7 @@ import teammates.common.util.Config; import teammates.common.util.Const; import teammates.common.util.EmailWrapper; +import teammates.common.util.HibernateUtil; import teammates.common.util.JsonUtils; import teammates.it.test.BaseTestCaseWithSqlDatabaseAccess; import teammates.logic.api.MockEmailSender; @@ -169,6 +170,14 @@ protected void loginAsAdmin() { assertTrue(user.isAdmin); } + /** + * Logs in the user to the test environment as an admin. + */ + protected void loginAsAdminWithTransaction() { + UserInfo user = mockUserProvision.loginAsAdminWithTransaction(Config.APP_ADMINS.get(0)); + assertTrue(user.isAdmin); + } + /** * Logs in the user to the test environment as an unregistered user * (without any right). @@ -180,6 +189,17 @@ protected void loginAsUnregistered(String userId) { assertFalse(user.isAdmin); } + /** + * Logs in the user to the test environment as an unregistered user + * (without any right). + */ + protected void loginAsUnregisteredWithTransaction(String userId) { + UserInfo user = mockUserProvision.loginUserWithTransaction(userId); + assertFalse(user.isStudent); + assertFalse(user.isInstructor); + assertFalse(user.isAdmin); + } + /** * Logs in the user to the test environment as an instructor * (without admin rights or student rights). @@ -191,6 +211,17 @@ protected void loginAsInstructor(String userId) { assertFalse(user.isAdmin); } + /** + * Logs in the user to the test environment as an instructor + * (without admin rights or student rights). + */ + protected void loginAsInstructorWithTransaction(String userId) { + UserInfo user = mockUserProvision.loginUserWithTransaction(userId); + assertFalse(user.isStudent); + assertTrue(user.isInstructor); + assertFalse(user.isAdmin); + } + /** * Logs in the user to the test environment as a student * (without admin rights or instructor rights). @@ -202,6 +233,17 @@ protected void loginAsStudent(String userId) { assertFalse(user.isAdmin); } + /** + * Logs in the user to the test environment as a student + * (without admin rights or instructor rights). + */ + protected void loginAsStudentWithTransaction(String userId) { + UserInfo user = mockUserProvision.loginUserWithTransaction(userId); + assertTrue(user.isStudent); + assertFalse(user.isInstructor); + assertFalse(user.isAdmin); + } + /** * Logs in the user to the test environment as a student-instructor (without * admin rights). @@ -267,6 +309,24 @@ void verifyOnlyAdminCanAccess(Course course, String... params) verifyAccessibleForAdmin(params); } + void verifyOnlyAdminCanAccessWithTransaction(String... params) + throws InvalidParametersException, EntityAlreadyExistsException { + HibernateUtil.beginTransaction(); + Course course = getTypicalCourse(); + course = logic.createCourse(course); + HibernateUtil.commitTransaction(); + + verifyInaccessibleWithoutLogin(params); + verifyInaccessibleForUnregisteredUsersWithTransaction(params); + verifyInaccessibleForStudentsWithTransaction(course, params); + verifyInaccessibleForInstructorsWithTransaction(course, params); + verifyAccessibleForAdminWithTransaction(params); + + HibernateUtil.beginTransaction(); + logic.deleteCourseCascade(course.getId()); + HibernateUtil.commitTransaction(); + } + void verifyOnlyInstructorsCanAccess(Course course, String... params) throws InvalidParametersException, EntityAlreadyExistsException { verifyInaccessibleWithoutLogin(params); @@ -329,6 +389,14 @@ void verifyInaccessibleForUnregisteredUsers(String... params) { verifyCannotAccess(params); } + void verifyInaccessibleForUnregisteredUsersWithTransaction(String... params) { + ______TS("Non-registered users cannot access"); + + String unregUserId = "unreg.user"; + loginAsUnregisteredWithTransaction(unregUserId); + verifyCannotAccess(params); + } + void verifyAccessibleForAdmin(String... params) { ______TS("Admin can access"); @@ -336,6 +404,13 @@ void verifyAccessibleForAdmin(String... params) { verifyCanAccess(params); } + void verifyAccessibleForAdminWithTransaction(String... params) { + ______TS("Admin can access"); + + loginAsAdminWithTransaction(); + verifyCanAccess(params); + } + void verifyInaccessibleForAdmin(String... params) { ______TS("Admin cannot access"); @@ -353,6 +428,21 @@ void verifyInaccessibleForStudents(Course course, String... params) } + void verifyInaccessibleForStudentsWithTransaction(Course course, String... params) + throws InvalidParametersException, EntityAlreadyExistsException { + ______TS("Students cannot access"); + HibernateUtil.beginTransaction(); + Student student = createTypicalStudent(course, "InaccessibleForStudents@teammates.tmt"); + HibernateUtil.commitTransaction(); + + loginAsStudentWithTransaction(student.getAccount().getGoogleId()); + verifyCannotAccess(params); + + HibernateUtil.beginTransaction(); + logic.deleteAccountCascade(student.getAccount().getGoogleId()); + HibernateUtil.commitTransaction(); + } + void verifyInaccessibleForInstructors(Course course, String... params) throws InvalidParametersException, EntityAlreadyExistsException { ______TS("Instructors cannot access"); @@ -363,6 +453,21 @@ void verifyInaccessibleForInstructors(Course course, String... params) } + void verifyInaccessibleForInstructorsWithTransaction(Course course, String... params) + throws InvalidParametersException, EntityAlreadyExistsException { + ______TS("Instructors cannot access"); + HibernateUtil.beginTransaction(); + Instructor instructor = createTypicalInstructor(course, "InaccessibleForInstructors@teammates.tmt"); + HibernateUtil.commitTransaction(); + + loginAsInstructorWithTransaction(instructor.getAccount().getGoogleId()); + verifyCannotAccess(params); + + HibernateUtil.beginTransaction(); + logic.deleteAccountCascade(instructor.getAccount().getGoogleId()); + HibernateUtil.commitTransaction(); + } + void verifyAccessibleForAdminToMasqueradeAsInstructor( Instructor instructor, String[] submissionParams) { ______TS("admin can access"); @@ -738,5 +843,4 @@ private Student createTypicalStudent(Course course, String email) } return student; } - } diff --git a/src/it/java/teammates/it/ui/webapi/CreateAccountActionIT.java b/src/it/java/teammates/it/ui/webapi/CreateAccountActionIT.java index 9bf4c3b76fcd..1b24bc094a23 100644 --- a/src/it/java/teammates/it/ui/webapi/CreateAccountActionIT.java +++ b/src/it/java/teammates/it/ui/webapi/CreateAccountActionIT.java @@ -4,6 +4,8 @@ import java.time.ZoneId; import java.util.List; +import jakarta.transaction.Transactional; + import org.testng.annotations.BeforeMethod; import org.testng.annotations.Ignore; import org.testng.annotations.Test; @@ -23,8 +25,6 @@ import teammates.ui.webapi.CreateAccountAction; import teammates.ui.webapi.InvalidHttpParameterException; -import jakarta.transaction.Transactional; - /** * SUT: {@link CreateAccountAction}. */ @@ -75,7 +75,7 @@ protected void testExecute() throws InvalidParametersException, EntityAlreadyExi ______TS("Normal case with valid timezone"); String timezone = "Asia/Singapore"; - AccountRequest accountRequest = logic.getAccountRequest(email, institute); + AccountRequest accountRequest = logic.getAccountRequest(accReq.getId()); String[] params = new String[] { Const.ParamsNames.REGKEY, accountRequest.getRegistrationKey(), @@ -118,10 +118,9 @@ protected void testExecute() throws InvalidParametersException, EntityAlreadyExi accReq = typicalBundle.accountRequests.get("unregisteredInstructor2"); email = accReq.getEmail(); - institute = accReq.getInstitute(); timezone = "InvalidTimezone"; - accountRequest = logic.getAccountRequest(email, institute); + accountRequest = logic.getAccountRequest(accReq.getId()); params = new String[] { Const.ParamsNames.REGKEY, accountRequest.getRegistrationKey(), diff --git a/src/it/java/teammates/it/ui/webapi/CreateAccountRequestActionIT.java b/src/it/java/teammates/it/ui/webapi/CreateAccountRequestActionIT.java index dcb1c279ae31..bc1ee01ad0fe 100644 --- a/src/it/java/teammates/it/ui/webapi/CreateAccountRequestActionIT.java +++ b/src/it/java/teammates/it/ui/webapi/CreateAccountRequestActionIT.java @@ -1,15 +1,21 @@ package teammates.it.ui.webapi; +import java.util.List; + +import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.exception.InvalidParametersException; import teammates.common.util.Const; import teammates.common.util.EmailType; import teammates.common.util.EmailWrapper; import teammates.common.util.HibernateUtil; import teammates.storage.sqlentity.AccountRequest; -import teammates.ui.output.JoinLinkData; +import teammates.ui.output.AccountRequestData; import teammates.ui.request.AccountCreateRequest; +import teammates.ui.request.InvalidHttpRequestBodyException; import teammates.ui.webapi.CreateAccountRequestAction; import teammates.ui.webapi.JsonResult; @@ -36,36 +42,214 @@ protected void setUp() { } @Override - @Test protected void testExecute() throws Exception { - // This is a minimal test; other cases are not tested due to upcoming changes in behaviour. + // This is separated into different test methods. + } + + @Test + void testExecute_nullEmail_throwsInvalidHttpRequestBodyException() { + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorName("Paul Atreides"); + request.setInstructorInstitution("House Atreides"); + InvalidHttpRequestBodyException ihrbException = verifyHttpRequestBodyFailure(request); + assertEquals("email cannot be null", ihrbException.getMessage()); + } + + @Test + void testExecute_nullName_throwsInvalidHttpRequestBodyException() { + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("kwisatz.haderach@atreides.org"); + request.setInstructorInstitution("House Atreides"); + InvalidHttpRequestBodyException ihrbException = verifyHttpRequestBodyFailure(request); + assertEquals("name cannot be null", ihrbException.getMessage()); + } + + @Test + void testExecute_nullInstitute_throwsInvalidHttpRequestBodyException() { + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("kwisatz.haderach@atreides.org"); + request.setInstructorName("Paul Atreides"); + InvalidHttpRequestBodyException ihrbException = verifyHttpRequestBodyFailure(request); + assertEquals("institute cannot be null", ihrbException.getMessage()); + } + + @Test + void testExecute_invalidEmail_throwsInvalidHttpRequestBodyException() { + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("invalid email address"); + request.setInstructorName("Paul Atreides"); + request.setInstructorInstitution("House Atreides"); + InvalidHttpRequestBodyException ihrbException = verifyHttpRequestBodyFailure(request); + String expectedMessage = "\"invalid email address\" is not acceptable to TEAMMATES as a/an email because it is not " + + "in the correct format. An email address contains some text followed by one '@' sign followed by some " + + "more text, and should end with a top level domain address like .com. It cannot be longer than 254 " + + "characters, cannot be empty and cannot contain spaces."; + assertEquals(expectedMessage, ihrbException.getMessage()); + } + + @Test + void testExecute_invalidName_throwsInvalidHttpRequestBodyException() { + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("kwisatz.haderach@atreides.org"); + request.setInstructorName("Pau| Atreides"); + request.setInstructorInstitution("House Atreides"); + InvalidHttpRequestBodyException ihrbException = verifyHttpRequestBodyFailure(request); + String expectedMessage = "\"Pau| Atreides\" is not acceptable to TEAMMATES as a/an person name because it contains " + + "invalid characters. A/An person name must start with an alphanumeric character, and cannot contain any " + + "vertical bar (|) or percent sign (%)."; + assertEquals(expectedMessage, ihrbException.getMessage()); + } + + @Test + void testExecute_invalidInstitute_throwsInvalidHttpRequestBodyException() { + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("kwisatz.haderach@atreides.org"); + request.setInstructorName("Paul Atreides"); + request.setInstructorInstitution("House Atreide%"); + InvalidHttpRequestBodyException ihrbException = verifyHttpRequestBodyFailure(request); + String expectedMessage = "\"House Atreide%\" is not acceptable to TEAMMATES as a/an institute name because it " + + "contains invalid characters. A/An institute name must start with an alphanumeric character, and cannot " + + "contain any vertical bar (|) or percent sign (%)."; + assertEquals(expectedMessage, ihrbException.getMessage()); + } + + @Test + void testExecute_typicalCase_createsSuccessfully() { + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("kwisatz.haderach@atreides.org"); + request.setInstructorName("Paul Atreides"); + request.setInstructorInstitution("House Atreides"); + request.setInstructorComments("My road leads into the desert. I can see it."); + CreateAccountRequestAction action = getAction(request); + JsonResult result = getJsonResult(action); + AccountRequestData output = (AccountRequestData) result.getOutput(); + assertEquals("kwisatz.haderach@atreides.org", output.getEmail()); + assertEquals("Paul Atreides", output.getName()); + assertEquals("House Atreides", output.getInstitute()); + assertEquals(AccountRequestStatus.PENDING, output.getStatus()); + assertEquals("My road leads into the desert. I can see it.", output.getComments()); + assertNull(output.getRegisteredAt()); + HibernateUtil.beginTransaction(); + AccountRequest accountRequest = logic.getAccountRequestByRegistrationKey(output.getRegistrationKey()); + HibernateUtil.commitTransaction(); + assertEquals("kwisatz.haderach@atreides.org", accountRequest.getEmail()); + assertEquals("Paul Atreides", accountRequest.getName()); + assertEquals("House Atreides", accountRequest.getInstitute()); + assertEquals(AccountRequestStatus.PENDING, accountRequest.getStatus()); + assertEquals("My road leads into the desert. I can see it.", accountRequest.getComments()); + assertNull(accountRequest.getRegisteredAt()); + verifySpecifiedTasksAdded(Const.TaskQueue.SEARCH_INDEXING_QUEUE_NAME, 1); + verifyNumberOfEmailsSent(2); + EmailWrapper sentAdminAlertEmail = mockEmailSender.getEmailsSent().get(0); + EmailWrapper sentAcknowledgementEmail = mockEmailSender.getEmailsSent().get(1); + assertEquals(EmailType.NEW_ACCOUNT_REQUEST_ADMIN_ALERT, sentAdminAlertEmail.getType()); + assertEquals(EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT, sentAcknowledgementEmail.getType()); + } + + @Test + void testExecute_leadingAndTrailingSpacesAndNullComments_createsSuccessfully() { AccountCreateRequest request = new AccountCreateRequest(); - request.setInstructorEmail("ring-bearer@fellowship.net"); - request.setInstructorName("Frodo Baggins"); - request.setInstructorInstitution("The Fellowship of the Ring"); + request.setInstructorEmail(" kwisatz.haderach@atreides.org "); + request.setInstructorName(" Paul Atreides "); + request.setInstructorInstitution(" House Atreides "); CreateAccountRequestAction action = getAction(request); JsonResult result = getJsonResult(action); - JoinLinkData output = (JoinLinkData) result.getOutput(); + AccountRequestData output = (AccountRequestData) result.getOutput(); + assertEquals("kwisatz.haderach@atreides.org", output.getEmail()); + assertEquals("Paul Atreides", output.getName()); + assertEquals("House Atreides", output.getInstitute()); + assertEquals(AccountRequestStatus.PENDING, output.getStatus()); + assertNull(output.getComments()); + assertNull(output.getRegisteredAt()); HibernateUtil.beginTransaction(); - AccountRequest accountRequest = logic.getAccountRequest("ring-bearer@fellowship.net", "The Fellowship of the Ring"); + AccountRequest accountRequest = logic.getAccountRequestByRegistrationKey(output.getRegistrationKey()); HibernateUtil.commitTransaction(); - assertEquals("ring-bearer@fellowship.net", accountRequest.getEmail()); - assertEquals("Frodo Baggins", accountRequest.getName()); - assertEquals("The Fellowship of the Ring", accountRequest.getInstitute()); + assertEquals("kwisatz.haderach@atreides.org", accountRequest.getEmail()); + assertEquals("Paul Atreides", accountRequest.getName()); + assertEquals("House Atreides", accountRequest.getInstitute()); + assertEquals(AccountRequestStatus.PENDING, accountRequest.getStatus()); + assertNull(accountRequest.getComments()); assertNull(accountRequest.getRegisteredAt()); - assertEquals(accountRequest.getRegistrationUrl(), output.getJoinLink()); - verifyNumberOfEmailsSent(1); verifySpecifiedTasksAdded(Const.TaskQueue.SEARCH_INDEXING_QUEUE_NAME, 1); - EmailWrapper emailSent = mockEmailSender.getEmailsSent().get(0); - assertEquals(String.format(EmailType.NEW_INSTRUCTOR_ACCOUNT.getSubject(), "Frodo Baggins"), - emailSent.getSubject()); - assertEquals("ring-bearer@fellowship.net", emailSent.getRecipient()); - assertTrue(emailSent.getContent().contains(output.getJoinLink())); + verifyNumberOfEmailsSent(2); + EmailWrapper sentAdminAlertEmail = mockEmailSender.getEmailsSent().get(0); + EmailWrapper sentAcknowledgementEmail = mockEmailSender.getEmailsSent().get(1); + assertEquals(EmailType.NEW_ACCOUNT_REQUEST_ADMIN_ALERT, sentAdminAlertEmail.getType()); + assertEquals(EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT, sentAcknowledgementEmail.getType()); + } + + @Test + void testExecute_accountRequestWithSameEmailAddressAndInstituteAlreadyExists_createsSuccessfully() + throws InvalidParametersException { + HibernateUtil.beginTransaction(); + AccountRequest existingAccountRequest = logic.createAccountRequest("Paul Atreides", + "kwisatz.haderach@atreides.org", + "House Atreides", AccountRequestStatus.PENDING, "My road leads into the desert. I can see it."); + HibernateUtil.commitTransaction(); + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("kwisatz.haderach@atreides.org"); + request.setInstructorName("Paul Atreides"); + request.setInstructorInstitution("House Atreides"); + request.setInstructorComments("My road leads into the desert. I can see it."); + CreateAccountRequestAction action = getAction(request); + JsonResult result = getJsonResult(action); + AccountRequestData output = (AccountRequestData) result.getOutput(); + assertEquals("kwisatz.haderach@atreides.org", output.getEmail()); + assertEquals("Paul Atreides", output.getName()); + assertEquals("House Atreides", output.getInstitute()); + assertEquals(AccountRequestStatus.PENDING, output.getStatus()); + assertEquals("My road leads into the desert. I can see it.", output.getComments()); + assertNull(output.getRegisteredAt()); + assertNotEquals(output.getRegistrationKey(), existingAccountRequest.getRegistrationKey()); + HibernateUtil.beginTransaction(); + AccountRequest accountRequest = logic.getAccountRequestByRegistrationKey(output.getRegistrationKey()); + HibernateUtil.commitTransaction(); + assertEquals("kwisatz.haderach@atreides.org", accountRequest.getEmail()); + assertEquals("Paul Atreides", accountRequest.getName()); + assertEquals("House Atreides", accountRequest.getInstitute()); + assertEquals(AccountRequestStatus.PENDING, accountRequest.getStatus()); + assertEquals("My road leads into the desert. I can see it.", accountRequest.getComments()); + assertNull(accountRequest.getRegisteredAt()); + verifySpecifiedTasksAdded(Const.TaskQueue.SEARCH_INDEXING_QUEUE_NAME, 1); + verifyNumberOfEmailsSent(2); + EmailWrapper sentAdminAlertEmail = mockEmailSender.getEmailsSent().get(0); + EmailWrapper sentAcknowledgementEmail = mockEmailSender.getEmailsSent().get(1); + assertEquals(EmailType.NEW_ACCOUNT_REQUEST_ADMIN_ALERT, sentAdminAlertEmail.getType()); + assertEquals(EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT, sentAcknowledgementEmail.getType()); + } + + @Test + void testExecute_typicalCaseAsAdmin_noEmailsSent() { + loginAsAdminWithTransaction(); + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("kwisatz.haderach@atreides.org"); + request.setInstructorName("Paul Atreides"); + request.setInstructorInstitution("House Atreides"); + request.setInstructorComments("My road leads into the desert. I can see it."); + CreateAccountRequestAction action = getAction(request); + JsonResult result = getJsonResult(action); + AccountRequestData output = (AccountRequestData) result.getOutput(); + assertNull(output.getRegisteredAt()); + verifyNoEmailsSent(); + logoutUser(); } @Override + @Test protected void testAccessControl() throws Exception { - // This is not tested due to upcoming changes in behaviour. + verifyAccessibleWithoutLogin(); } + @Override + @AfterMethod + protected void tearDown() { + HibernateUtil.beginTransaction(); + List accountRequests = logic.getPendingAccountRequests(); + for (AccountRequest ar : accountRequests) { + logic.deleteAccountRequest(ar.getId()); + } + accountRequests = logic.getPendingAccountRequests(); + HibernateUtil.commitTransaction(); + assert accountRequests.isEmpty(); + } } diff --git a/src/it/java/teammates/it/ui/webapi/CreateFeedbackSessionLogActionIT.java b/src/it/java/teammates/it/ui/webapi/CreateFeedbackSessionLogActionIT.java new file mode 100644 index 000000000000..c15c45752cc5 --- /dev/null +++ b/src/it/java/teammates/it/ui/webapi/CreateFeedbackSessionLogActionIT.java @@ -0,0 +1,160 @@ +package teammates.it.ui.webapi; + +import java.util.UUID; + +import org.testng.annotations.Test; + +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.util.Const; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.Student; +import teammates.ui.output.MessageOutput; +import teammates.ui.webapi.CreateFeedbackSessionLogAction; +import teammates.ui.webapi.JsonResult; + +/** + * SUT: {@link CreateFeedbackSessionLogAction}. + */ +public class CreateFeedbackSessionLogActionIT extends BaseActionIT { + + @Override + protected String getActionUri() { + return Const.ResourceURIs.SESSION_LOGS; + } + + @Override + protected String getRequestMethod() { + return POST; + } + + @Test + @Override + protected void testExecute() throws Exception { + Course course1 = typicalBundle.courses.get("course1"); + String courseId1 = course1.getId(); + FeedbackSession fs1 = typicalBundle.feedbackSessions.get("session1InCourse1"); + FeedbackSession fs2 = typicalBundle.feedbackSessions.get("session2InTypicalCourse"); + Student student1 = typicalBundle.students.get("student1InCourse1"); + Student student2 = typicalBundle.students.get("student2InCourse1"); + Student student3 = typicalBundle.students.get("student1InCourse3"); + + ______TS("Failure case: not enough parameters"); + verifyHttpParameterFailure(Const.ParamsNames.COURSE_ID, courseId1); + verifyHttpParameterFailure( + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName() + ); + verifyHttpParameterFailure( + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, student1.getEmail() + ); + verifyHttpParameterFailure( + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.ACCESS.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, student1.getEmail() + ); + verifyHttpParameterFailure( + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_ID, fs2.getId().toString(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), + Const.ParamsNames.STUDENT_SQL_ID, student2.getId().toString() + ); + + ______TS("Failure case: invalid log type"); + String[] paramsInvalid = { + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, "invalid log type", + Const.ParamsNames.STUDENT_EMAIL, student1.getEmail(), + Const.ParamsNames.FEEDBACK_SESSION_ID, fs1.getId().toString(), + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), + }; + verifyHttpParameterFailure(paramsInvalid); + + ______TS("Success case: typical access"); + String[] paramsSuccessfulAccess = { + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.ACCESS.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, student1.getEmail(), + Const.ParamsNames.FEEDBACK_SESSION_ID, fs1.getId().toString(), + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), + }; + JsonResult response = getJsonResult(getAction(paramsSuccessfulAccess)); + MessageOutput output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); + + ______TS("Success case: typical submission"); + String[] paramsSuccessfulSubmission = { + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs2.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, student2.getEmail(), + Const.ParamsNames.FEEDBACK_SESSION_ID, fs2.getId().toString(), + Const.ParamsNames.STUDENT_SQL_ID, student2.getId().toString(), + }; + response = getJsonResult(getAction(paramsSuccessfulSubmission)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); + + ______TS("Success case: should create even for invalid parameters"); + String[] paramsNonExistentCourseId = { + Const.ParamsNames.COURSE_ID, "non-existent-course-id", + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, student1.getEmail(), + Const.ParamsNames.FEEDBACK_SESSION_ID, fs1.getId().toString(), + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), + }; + response = getJsonResult(getAction(paramsNonExistentCourseId)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); + + ______TS("Success case: should create even for invalid parameters"); + String[] paramsNonExistentFsName = { + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, "non-existent-feedback-session-name", + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, student1.getEmail(), + Const.ParamsNames.FEEDBACK_SESSION_ID, UUID.randomUUID().toString(), + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), + }; + response = getJsonResult(getAction(paramsNonExistentFsName)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); + + String[] paramsNonExistentStudentEmail = { + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, "non-existent-student@email.com", + Const.ParamsNames.FEEDBACK_SESSION_ID, fs1.getId().toString(), + Const.ParamsNames.STUDENT_SQL_ID, UUID.randomUUID().toString(), + }; + response = getJsonResult(getAction(paramsNonExistentStudentEmail)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); + + ______TS("Success case: should create even when student cannot access feedback session in course"); + String[] paramsWithoutAccess = { + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, student3.getEmail(), + Const.ParamsNames.FEEDBACK_SESSION_ID, fs1.getId().toString(), + Const.ParamsNames.STUDENT_SQL_ID, student3.getId().toString(), + }; + response = getJsonResult(getAction(paramsWithoutAccess)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); + } + + @Test + @Override + protected void testAccessControl() throws Exception { + verifyAnyUserCanAccess(); + } +} diff --git a/src/it/java/teammates/it/ui/webapi/GetAccountRequestsActionIT.java b/src/it/java/teammates/it/ui/webapi/GetAccountRequestsActionIT.java new file mode 100644 index 000000000000..feecca657470 --- /dev/null +++ b/src/it/java/teammates/it/ui/webapi/GetAccountRequestsActionIT.java @@ -0,0 +1,109 @@ +package teammates.it.ui.webapi; + +import java.util.List; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.exception.EntityAlreadyExistsException; +import teammates.common.exception.InvalidParametersException; +import teammates.common.util.Const; +import teammates.common.util.HibernateUtil; +import teammates.storage.sqlentity.AccountRequest; +import teammates.storage.sqlentity.Course; +import teammates.ui.output.AccountRequestData; +import teammates.ui.output.AccountRequestsData; +import teammates.ui.webapi.GetAccountRequestsAction; +import teammates.ui.webapi.JsonResult; + +/** + * SUT: {@link GetAccountRequestsAction}. + */ +public class GetAccountRequestsActionIT extends BaseActionIT { + private final String[] validParams = { Const.ParamsNames.ACCOUNT_REQUEST_STATUS, "pending" }; + + @Override + @BeforeMethod + protected void setUp() throws Exception { + super.setUp(); + persistDataBundle(typicalBundle); + HibernateUtil.flushSession(); + } + + @Override + protected String getActionUri() { + return Const.ResourceURIs.ACCOUNT_REQUESTS; + } + + @Override + protected String getRequestMethod() { + return GET; + } + + @Override + @Test + public void testExecute() { + ______TS("accountrequeststatus param is null"); + + verifyHttpParameterFailure(); + verifyHttpParameterFailure(Const.ParamsNames.ACCOUNT_REQUEST_STATUS, "pendin"); + + ______TS("No pending account requests initially"); + + GetAccountRequestsAction action = getAction(this.validParams); + JsonResult result = getJsonResult(action); + AccountRequestsData data = (AccountRequestsData) result.getOutput(); + List arData = data.getAccountRequests(); + + assertEquals(0, arData.size()); + + ______TS("1 pending account request, case insensitive match for status request param"); + + AccountRequest accountRequest1 = typicalBundle.accountRequests.get("instructor1"); + accountRequest1.setStatus(AccountRequestStatus.PENDING); + + String[] params = { Const.ParamsNames.ACCOUNT_REQUEST_STATUS, "PendinG" }; + action = getAction(params); + result = getJsonResult(action); + data = (AccountRequestsData) result.getOutput(); + arData = data.getAccountRequests(); + + assertEquals(1, arData.size()); + + ______TS("Get 2 pending account requests, ignore 1 approved account request"); + AccountRequest approvedAccountRequest1 = typicalBundle.accountRequests.get("instructor2"); + approvedAccountRequest1.setStatus(AccountRequestStatus.APPROVED); + + accountRequest1 = typicalBundle.accountRequests.get("instructor1"); + AccountRequest accountRequest2 = typicalBundle.accountRequests.get("instructor1OfCourse2"); + accountRequest1.setStatus(AccountRequestStatus.PENDING); + accountRequest2.setStatus(AccountRequestStatus.PENDING); + + action = getAction(this.validParams); + result = getJsonResult(action); + data = (AccountRequestsData) result.getOutput(); + arData = data.getAccountRequests(); + + assertEquals(2, arData.size()); + + // account request 1 (with the most recent created_at) + assertEquals(arData.get(1).getEmail(), accountRequest1.getEmail()); + assertEquals(arData.get(1).getInstitute(), accountRequest1.getInstitute()); + assertEquals(arData.get(1).getName(), accountRequest1.getName()); + assertEquals(arData.get(1).getRegistrationKey(), accountRequest1.getRegistrationKey()); + + // account request 2 + assertEquals(arData.get(0).getEmail(), accountRequest2.getEmail()); + assertEquals(arData.get(0).getInstitute(), accountRequest2.getInstitute()); + assertEquals(arData.get(0).getName(), accountRequest2.getName()); + assertEquals(arData.get(0).getRegistrationKey(), accountRequest2.getRegistrationKey()); + } + + @Override + @Test + public void testAccessControl() throws InvalidParametersException, EntityAlreadyExistsException { + Course course = typicalBundle.courses.get("course1"); + verifyOnlyAdminCanAccess(course); + } +} diff --git a/src/it/java/teammates/it/ui/webapi/GetCourseJoinStatusActionIT.java b/src/it/java/teammates/it/ui/webapi/GetCourseJoinStatusActionIT.java index 4f024306105e..2c322b233941 100644 --- a/src/it/java/teammates/it/ui/webapi/GetCourseJoinStatusActionIT.java +++ b/src/it/java/teammates/it/ui/webapi/GetCourseJoinStatusActionIT.java @@ -5,6 +5,7 @@ import teammates.common.util.Const; import teammates.common.util.HibernateUtil; +import teammates.storage.sqlentity.AccountRequest; import teammates.ui.output.JoinStatus; import teammates.ui.webapi.GetCourseJoinStatusAction; import teammates.ui.webapi.JsonResult; @@ -131,8 +132,8 @@ protected void testExecute() { ______TS("Normal case: account request not used, instructor has not joined course"); - String accountRequestNotUsedKey = logic.getAccountRequest("unregisteredinstructor1@gmail.tmt", - "TEAMMATES Test Institute 1").getRegistrationKey(); + AccountRequest unregisteredInstructor1AccountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1"); + String accountRequestNotUsedKey = unregisteredInstructor1AccountRequest.getRegistrationKey(); params = new String[] { Const.ParamsNames.REGKEY, accountRequestNotUsedKey, @@ -148,8 +149,8 @@ protected void testExecute() { ______TS("Normal case: account request already used, instructor has joined course"); - String accountRequestUsedKey = - logic.getAccountRequest("instr1@teammates.tmt", "TEAMMATES Test Institute 1").getRegistrationKey(); + AccountRequest instructor1AccountRequest = typicalBundle.accountRequests.get("instructor1"); + String accountRequestUsedKey = instructor1AccountRequest.getRegistrationKey(); params = new String[] { Const.ParamsNames.REGKEY, accountRequestUsedKey, diff --git a/src/it/java/teammates/it/ui/webapi/GetCoursesActionIT.java b/src/it/java/teammates/it/ui/webapi/GetCoursesActionIT.java index 408821c38b72..b10149b70b07 100644 --- a/src/it/java/teammates/it/ui/webapi/GetCoursesActionIT.java +++ b/src/it/java/teammates/it/ui/webapi/GetCoursesActionIT.java @@ -122,6 +122,7 @@ public void testGetCoursesAction_withStudentEntityType_shouldReturnCorrectCourse loginAsStudent(student.getGoogleId()); CoursesData courses = getValidCourses(params); + courses.getCourses().sort((c1, c2) -> c1.getCourseId().compareTo(c2.getCourseId())); assertEquals(3, courses.getCourses().size()); Course expectedCourse1 = typicalBundle.courses.get("typicalCourse1"); Course expectedCourse2 = typicalBundle.courses.get("typicalCourse2"); diff --git a/src/it/java/teammates/it/ui/webapi/GetFeedbackSessionLogsActionIT.java b/src/it/java/teammates/it/ui/webapi/GetFeedbackSessionLogsActionIT.java index 2ee319637ae9..238e91754655 100644 --- a/src/it/java/teammates/it/ui/webapi/GetFeedbackSessionLogsActionIT.java +++ b/src/it/java/teammates/it/ui/webapi/GetFeedbackSessionLogsActionIT.java @@ -49,27 +49,12 @@ protected void testExecute() { Course course = typicalBundle.courses.get("course1"); String courseId = course.getId(); FeedbackSession fsa1 = typicalBundle.feedbackSessions.get("session1InCourse1"); - FeedbackSession fsa2 = typicalBundle.feedbackSessions.get("session2InTypicalCourse"); - String fsa1Name = fsa1.getName(); - String fsa2Name = fsa2.getName(); Student student1 = typicalBundle.students.get("student1InCourse1"); Student student2 = typicalBundle.students.get("student2InCourse1"); String student1Email = student1.getEmail(); String student2Email = student2.getEmail(); - long endTime = Instant.now().toEpochMilli(); + long endTime = Instant.parse("2012-01-02T12:00:00Z").toEpochMilli(); long startTime = endTime - (Const.LOGS_RETENTION_PERIOD.toDays() - 1) * 24 * 60 * 60 * 1000; - long invalidStartTime = endTime - (Const.LOGS_RETENTION_PERIOD.toDays() + 1) * 24 * 60 * 60 * 1000; - - mockLogsProcessor.insertFeedbackSessionLog(student1Email, fsa1Name, - FeedbackSessionLogType.ACCESS.getLabel(), startTime); - mockLogsProcessor.insertFeedbackSessionLog(student1Email, fsa2Name, - FeedbackSessionLogType.ACCESS.getLabel(), startTime + 1000); - mockLogsProcessor.insertFeedbackSessionLog(student1Email, fsa2Name, - FeedbackSessionLogType.SUBMISSION.getLabel(), startTime + 2000); - mockLogsProcessor.insertFeedbackSessionLog(student2Email, fsa1Name, - FeedbackSessionLogType.ACCESS.getLabel(), startTime + 3000); - mockLogsProcessor.insertFeedbackSessionLog(student2Email, fsa1Name, - FeedbackSessionLogType.SUBMISSION.getLabel(), startTime + 4000); ______TS("Failure case: not enough parameters"); verifyHttpParameterFailure( @@ -87,16 +72,16 @@ protected void testExecute() { ______TS("Failure case: invalid course id"); String[] paramsInvalid1 = { Const.ParamsNames.COURSE_ID, "fake-course-id", - Const.ParamsNames.STUDENT_EMAIL, student1Email, + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), }; verifyEntityNotFound(paramsInvalid1); - ______TS("Failure case: invalid student email"); + ______TS("Failure case: invalid student id"); String[] paramsInvalid2 = { Const.ParamsNames.COURSE_ID, courseId, - Const.ParamsNames.STUDENT_EMAIL, "fake-student-email@gmail.com", + Const.ParamsNames.STUDENT_SQL_ID, "00000000-0000-0000-0000-000000000000", Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), }; @@ -117,13 +102,6 @@ protected void testExecute() { }; verifyHttpParameterFailure(paramsInvalid4); - ______TS("Failure case: start time is before earliest search time"); - verifyHttpParameterFailure( - Const.ParamsNames.COURSE_ID, courseId, - Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(invalidStartTime), - Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime) - ); - ______TS("Success case: should group by feedback session"); String[] paramsSuccessful1 = { Const.ParamsNames.COURSE_ID, courseId, @@ -161,15 +139,63 @@ protected void testExecute() { assertEquals(fsLogEntries2.get(1).getStudentData().getEmail(), student1Email); assertEquals(fsLogEntries2.get(1).getFeedbackSessionLogType(), FeedbackSessionLogType.SUBMISSION); - ______TS("Success case: should accept optional email"); + ______TS("Success case: should accept optional student Id"); String[] paramsSuccessful2 = { Const.ParamsNames.COURSE_ID, courseId, - Const.ParamsNames.STUDENT_EMAIL, student1Email, + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), }; - getJsonResult(getAction(paramsSuccessful2)); - // No need to check output again here, it will be exactly the same as the previous case + actionOutput = getJsonResult(getAction(paramsSuccessful2)); + fslData = (FeedbackSessionLogsData) actionOutput.getOutput(); + fsLogs = fslData.getFeedbackSessionLogs(); + + assertEquals(fsLogs.size(), 6); + assertEquals(fsLogs.get(2).getFeedbackSessionLogEntries().size(), 0); + assertEquals(fsLogs.get(3).getFeedbackSessionLogEntries().size(), 0); + assertEquals(fsLogs.get(4).getFeedbackSessionLogEntries().size(), 0); + assertEquals(fsLogs.get(5).getFeedbackSessionLogEntries().size(), 0); + + fsLogEntries1 = fsLogs.get(0).getFeedbackSessionLogEntries(); + fsLogEntries2 = fsLogs.get(1).getFeedbackSessionLogEntries(); + + assertEquals(fsLogEntries1.size(), 1); + assertEquals(fsLogEntries1.get(0).getStudentData().getEmail(), student1Email); + assertEquals(fsLogEntries1.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + + assertEquals(fsLogEntries2.size(), 2); + assertEquals(fsLogEntries2.get(0).getStudentData().getEmail(), student1Email); + assertEquals(fsLogEntries2.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries2.get(1).getStudentData().getEmail(), student1Email); + assertEquals(fsLogEntries2.get(1).getFeedbackSessionLogType(), FeedbackSessionLogType.SUBMISSION); + + ______TS("Success case: should accept optional feedback session"); + String[] paramsSuccessful3 = { + Const.ParamsNames.COURSE_ID, courseId, + Const.ParamsNames.FEEDBACK_SESSION_ID, fsa1.getId().toString(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + actionOutput = getJsonResult(getAction(paramsSuccessful3)); + fslData = (FeedbackSessionLogsData) actionOutput.getOutput(); + fsLogs = fslData.getFeedbackSessionLogs(); + + assertEquals(fsLogs.size(), 6); + assertEquals(fsLogs.get(1).getFeedbackSessionLogEntries().size(), 0); + assertEquals(fsLogs.get(2).getFeedbackSessionLogEntries().size(), 0); + assertEquals(fsLogs.get(3).getFeedbackSessionLogEntries().size(), 0); + assertEquals(fsLogs.get(4).getFeedbackSessionLogEntries().size(), 0); + assertEquals(fsLogs.get(5).getFeedbackSessionLogEntries().size(), 0); + + fsLogEntries1 = fsLogs.get(0).getFeedbackSessionLogEntries(); + + assertEquals(fsLogEntries1.size(), 3); + assertEquals(fsLogEntries1.get(0).getStudentData().getEmail(), student1Email); + assertEquals(fsLogEntries1.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries1.get(1).getStudentData().getEmail(), student2Email); + assertEquals(fsLogEntries1.get(1).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries1.get(2).getStudentData().getEmail(), student2Email); + assertEquals(fsLogEntries1.get(2).getFeedbackSessionLogType(), FeedbackSessionLogType.SUBMISSION); // TODO: if we restrict the range from start to end time, it should be tested here as well } diff --git a/src/it/java/teammates/it/ui/webapi/GetSessionResultsActionIT.java b/src/it/java/teammates/it/ui/webapi/GetSessionResultsActionIT.java index 246ea81b5a45..a7abb8352127 100644 --- a/src/it/java/teammates/it/ui/webapi/GetSessionResultsActionIT.java +++ b/src/it/java/teammates/it/ui/webapi/GetSessionResultsActionIT.java @@ -12,6 +12,7 @@ import teammates.common.util.HibernateUtil; import teammates.common.util.JsonUtils; import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackSession; import teammates.storage.sqlentity.Instructor; import teammates.storage.sqlentity.Section; @@ -153,6 +154,32 @@ protected void testExecute() { student); assertTrue(isSessionResultsDataEqual(expectedResults, output)); + + ______TS("Typical: Student accesses results of their course by questionId"); + + loginAsStudent(student.getGoogleId()); + + FeedbackQuestion question = typicalBundle.feedbackQuestions.get("qn1InSession1InCourse1"); + + submissionParams = new String[] { + Const.ParamsNames.FEEDBACK_SESSION_NAME, accessibleFeedbackSession.getName(), + Const.ParamsNames.COURSE_ID, accessibleFeedbackSession.getCourse().getId(), + Const.ParamsNames.FEEDBACK_QUESTION_ID, question.getId().toString(), + Const.ParamsNames.INTENT, Intent.STUDENT_RESULT.name(), + }; + + a = getAction(submissionParams); + r = getJsonResult(a); + + output = (SessionResultsData) r.getOutput(); + expectedResults = SessionResultsData.initForStudent( + logic.getSessionResultsForUser(accessibleFeedbackSession, + accessibleFeedbackSession.getCourse().getId(), + student.getEmail(), + false, question.getId(), false), + student); + + assertTrue(isSessionResultsDataEqual(expectedResults, output)); } @Override @@ -231,7 +258,7 @@ private boolean isSessionResultsDataEqual(SessionResultsData self, SessionResult } private boolean isQuestionOutputEqual(SessionResultsData.QuestionOutput self, - SessionResultsData.QuestionOutput other) { + SessionResultsData.QuestionOutput other) { if (!JsonUtils.toJson(self.getFeedbackQuestion()).equals(JsonUtils.toJson(other.getFeedbackQuestion())) || !self.getQuestionStatistics().equals(other.getQuestionStatistics()) || self.getHasResponseButNotVisibleForPreview() != other.getHasResponseButNotVisibleForPreview() @@ -254,7 +281,7 @@ private boolean isQuestionOutputEqual(SessionResultsData.QuestionOutput self, } private boolean isResponseOutputEqual(SessionResultsData.ResponseOutput self, - SessionResultsData.ResponseOutput other) { + SessionResultsData.ResponseOutput other) { return self.getGiver().equals(other.getGiver()) && self.getGiverTeam().equals(other.getGiverTeam()) && self.getGiverSection().equals(other.getGiverSection()) diff --git a/src/it/java/teammates/it/ui/webapi/RejectAccountRequestActionIT.java b/src/it/java/teammates/it/ui/webapi/RejectAccountRequestActionIT.java new file mode 100644 index 000000000000..78f25dd89e8e --- /dev/null +++ b/src/it/java/teammates/it/ui/webapi/RejectAccountRequestActionIT.java @@ -0,0 +1,229 @@ +package teammates.it.ui.webapi; + +import java.util.List; +import java.util.UUID; + +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.exception.EntityAlreadyExistsException; +import teammates.common.exception.InvalidParametersException; +import teammates.common.util.Config; +import teammates.common.util.Const; +import teammates.common.util.EmailType; +import teammates.common.util.EmailWrapper; +import teammates.common.util.HibernateUtil; +import teammates.common.util.SanitizationHelper; +import teammates.storage.sqlentity.AccountRequest; +import teammates.ui.output.AccountRequestData; +import teammates.ui.request.AccountRequestRejectionRequest; +import teammates.ui.request.InvalidHttpRequestBodyException; +import teammates.ui.webapi.EntityNotFoundException; +import teammates.ui.webapi.InvalidHttpParameterException; +import teammates.ui.webapi.InvalidOperationException; +import teammates.ui.webapi.JsonResult; +import teammates.ui.webapi.RejectAccountRequestAction; + +/** + * SUT: {@link RejectAccountRequestAction}. + */ +public class RejectAccountRequestActionIT extends BaseActionIT { + + private static final String TYPICAL_TITLE = "We are Unable to Create an Account for you"; + private static final String TYPICAL_BODY = new StringBuilder() + .append("

Hi, Example

\n") + .append("

Thanks for your interest in using TEAMMATES. ") + .append("We are unable to create a TEAMMATES instructor account for you.

\n\n") + .append("

\n") + .append(" Reason: The email address you provided ") + .append("is not an 'official' email address provided by your institution.
\n") + .append(" Remedy: ") + .append("Please re-submit an account request with your 'official' institution email address.\n") + .append("

\n\n") + .append("

If you need further clarification or would like to appeal this decision, ") + .append("please feel free to contact us at teammates@comp.nus.edu.sg.

\n") + .append("

Regards,
TEAMMATES Team.

\n") + .toString(); + + @Override + @BeforeMethod + protected void setUp() throws Exception { + // no need to call super.setUp() because the action handles its own transactions + } + + @Override + protected String getActionUri() { + return Const.ResourceURIs.ACCOUNT_REQUEST_REJECTION; + } + + @Override + protected String getRequestMethod() { + return POST; + } + + @Override + public void testExecute() throws Exception { + // See individual test methods below + } + + @Test + protected void testExecute_withReasonTitleAndBody_shouldRejectWithEmail() + throws InvalidOperationException, InvalidHttpRequestBodyException, InvalidParametersException { + AccountRequest bundleAccountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1"); + AccountRequest accountRequest = logic.createAccountRequestWithTransaction(bundleAccountRequest.getName(), + bundleAccountRequest.getEmail(), bundleAccountRequest.getInstitute(), + AccountRequestStatus.PENDING, bundleAccountRequest.getComments()); + UUID id = accountRequest.getId(); + + AccountRequestRejectionRequest requestBody = new AccountRequestRejectionRequest(TYPICAL_TITLE, TYPICAL_BODY); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + RejectAccountRequestAction action = getAction(requestBody, params); + JsonResult result = action.execute(); + + assertEquals(200, result.getStatusCode()); + + AccountRequestData data = (AccountRequestData) result.getOutput(); + assertEquals(accountRequest.getName(), data.getName()); + assertEquals(accountRequest.getEmail(), data.getEmail()); + assertEquals(accountRequest.getInstitute(), data.getInstitute()); + assertEquals(AccountRequestStatus.REJECTED, data.getStatus()); + assertEquals(accountRequest.getComments(), data.getComments()); + + verifyNumberOfEmailsSent(1); + EmailWrapper sentEmail = mockEmailSender.getEmailsSent().get(0); + assertEquals(EmailType.ACCOUNT_REQUEST_REJECTION, sentEmail.getType()); + assertEquals(Config.SUPPORT_EMAIL, sentEmail.getBcc()); + assertEquals(accountRequest.getEmail(), sentEmail.getRecipient()); + assertEquals(SanitizationHelper.sanitizeForRichText(TYPICAL_BODY), sentEmail.getContent()); + assertEquals("TEAMMATES: " + TYPICAL_TITLE, sentEmail.getSubject()); + } + + @Test + protected void testExecute_withoutReasonTitleAndBody_shouldRejectWithoutEmail() + throws InvalidOperationException, InvalidHttpRequestBodyException, InvalidParametersException { + AccountRequest bundleAccountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1"); + AccountRequest accountRequest = logic.createAccountRequestWithTransaction(bundleAccountRequest.getName(), + bundleAccountRequest.getEmail(), bundleAccountRequest.getInstitute(), + AccountRequestStatus.PENDING, bundleAccountRequest.getComments()); + UUID id = accountRequest.getId(); + + AccountRequestRejectionRequest requestBody = new AccountRequestRejectionRequest(null, null); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + RejectAccountRequestAction action = getAction(requestBody, params); + JsonResult result = action.execute(); + + assertEquals(200, result.getStatusCode()); + + AccountRequestData data = (AccountRequestData) result.getOutput(); + assertEquals(accountRequest.getName(), data.getName()); + assertEquals(accountRequest.getEmail(), data.getEmail()); + assertEquals(accountRequest.getInstitute(), data.getInstitute()); + assertEquals(AccountRequestStatus.REJECTED, data.getStatus()); + assertEquals(accountRequest.getComments(), data.getComments()); + + verifyNoEmailsSent(); + } + + @Test + protected void testExecute_withReasonBodyButNoTitle_shouldThrow() throws InvalidParametersException { + AccountRequest bundleAccountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1"); + AccountRequest accountRequest = logic.createAccountRequestWithTransaction(bundleAccountRequest.getName(), + bundleAccountRequest.getEmail(), bundleAccountRequest.getInstitute(), + bundleAccountRequest.getStatus(), bundleAccountRequest.getComments()); + UUID id = accountRequest.getId(); + + AccountRequestRejectionRequest requestBody = new AccountRequestRejectionRequest(null, TYPICAL_BODY); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + InvalidHttpRequestBodyException ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals("Both reason body and title need to be null to reject silently", ihrbe.getMessage()); + verifyNoEmailsSent(); + } + + @Test + protected void testExecute_withReasonTitleButNoBody_shouldThrow() throws InvalidParametersException { + AccountRequest bundleAccountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1"); + AccountRequest accountRequest = logic.createAccountRequestWithTransaction(bundleAccountRequest.getName(), + bundleAccountRequest.getEmail(), bundleAccountRequest.getInstitute(), + bundleAccountRequest.getStatus(), bundleAccountRequest.getComments()); + UUID id = accountRequest.getId(); + + AccountRequestRejectionRequest requestBody = new AccountRequestRejectionRequest(TYPICAL_TITLE, null); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + InvalidHttpRequestBodyException ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals("Both reason body and title need to be null to reject silently", ihrbe.getMessage()); + verifyNoEmailsSent(); + } + + @Test + protected void testExecute_alreadyRejected_shouldNotSendEmail() + throws InvalidOperationException, InvalidHttpRequestBodyException, InvalidParametersException { + AccountRequest bundleAccountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1"); + AccountRequest accountRequest = logic.createAccountRequestWithTransaction(bundleAccountRequest.getName(), + bundleAccountRequest.getEmail(), bundleAccountRequest.getInstitute(), + AccountRequestStatus.REJECTED, bundleAccountRequest.getComments()); + UUID id = accountRequest.getId(); + + AccountRequestRejectionRequest requestBody = new AccountRequestRejectionRequest(TYPICAL_TITLE, TYPICAL_BODY); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + RejectAccountRequestAction action = getAction(requestBody, params); + JsonResult result = action.execute(); + + assertEquals(result.getStatusCode(), 200); + + AccountRequestData data = (AccountRequestData) result.getOutput(); + assertEquals(accountRequest.getName(), data.getName()); + assertEquals(accountRequest.getEmail(), data.getEmail()); + assertEquals(accountRequest.getInstitute(), data.getInstitute()); + assertEquals(accountRequest.getStatus(), data.getStatus()); + assertEquals(accountRequest.getComments(), data.getComments()); + + verifyNoEmailsSent(); + } + + @Test + protected void testExecute_invalidUuid_shouldThrow() throws InvalidParametersException { + AccountRequestRejectionRequest requestBody = new AccountRequestRejectionRequest(null, null); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, "invalid"}; + + InvalidHttpParameterException ihpe = verifyHttpParameterFailure(requestBody, params); + assertEquals("Expected UUID value for id parameter, but found: [invalid]", ihpe.getMessage()); + verifyNoEmailsSent(); + } + + @Test + protected void testExecute_accountRequestNotFound_shouldThrow() { + AccountRequestRejectionRequest requestBody = new AccountRequestRejectionRequest(null, null); + String uuid = UUID.randomUUID().toString(); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, uuid}; + + EntityNotFoundException enfe = verifyEntityNotFound(requestBody, params); + assertEquals(String.format("Account request with id = %s not found", uuid), enfe.getMessage()); + verifyNoEmailsSent(); + } + + @Override + @Test + protected void testAccessControl() throws InvalidParametersException, EntityAlreadyExistsException { + verifyOnlyAdminCanAccessWithTransaction(); + } + + @Override + @AfterMethod + protected void tearDown() { + HibernateUtil.beginTransaction(); + List accountRequests = logic.getAllAccountRequests(); + for (AccountRequest ar : accountRequests) { + logic.deleteAccountRequest(ar.getId()); + } + HibernateUtil.commitTransaction(); + } +} diff --git a/src/it/java/teammates/it/ui/webapi/UpdateAccountRequestActionIT.java b/src/it/java/teammates/it/ui/webapi/UpdateAccountRequestActionIT.java new file mode 100644 index 000000000000..f5932deaf997 --- /dev/null +++ b/src/it/java/teammates/it/ui/webapi/UpdateAccountRequestActionIT.java @@ -0,0 +1,262 @@ +package teammates.it.ui.webapi; + +import java.util.List; +import java.util.UUID; + +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.AccountRequestStatus; +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.common.util.StringHelperExtension; +import teammates.storage.sqlentity.Account; +import teammates.storage.sqlentity.AccountRequest; +import teammates.ui.output.AccountRequestData; +import teammates.ui.request.AccountRequestUpdateRequest; +import teammates.ui.request.InvalidHttpRequestBodyException; +import teammates.ui.webapi.EntityNotFoundException; +import teammates.ui.webapi.InvalidHttpParameterException; +import teammates.ui.webapi.InvalidOperationException; +import teammates.ui.webapi.JsonResult; +import teammates.ui.webapi.UpdateAccountRequestAction; + +/** + * SUT: {@link UpdateAccountRequestAction}. + */ +public class UpdateAccountRequestActionIT extends BaseActionIT { + + @Override + @BeforeMethod + protected void setUp() throws Exception { + // no need to call super.setUp() because the action handles its own transactions + } + + @Override + protected String getActionUri() { + return Const.ResourceURIs.ACCOUNT_REQUEST; + } + + @Override + protected String getRequestMethod() { + return PUT; + } + + @Override + @Test + public void testExecute() throws Exception { + ______TS("edit fields of an account request"); + AccountRequest accountRequest = logic.createAccountRequestWithTransaction("name", "email@email.com", + "institute", AccountRequestStatus.PENDING, "comments"); + UUID id = accountRequest.getId(); + String name = "newName"; + String email = "newEmail@email.com"; + String institute = "newInstitute"; + String comments = "newComments"; + AccountRequestStatus status = accountRequest.getStatus(); + + AccountRequestUpdateRequest requestBody = new AccountRequestUpdateRequest(name, email, institute, status, comments); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + UpdateAccountRequestAction action = getAction(requestBody, params); + JsonResult result = action.execute(); + + assertEquals(result.getStatusCode(), 200); + AccountRequestData data = (AccountRequestData) result.getOutput(); + + assertEquals(name, data.getName()); + assertEquals(email, data.getEmail()); + assertEquals(institute, data.getInstitute()); + assertEquals(status, data.getStatus()); + assertEquals(comments, data.getComments()); + verifyNoEmailsSent(); + + ______TS("approve a pending account request"); + accountRequest = logic.createAccountRequestWithTransaction("name", "email@email.com", + "institute", AccountRequestStatus.PENDING, "comments"); + requestBody = new AccountRequestUpdateRequest(accountRequest.getName(), accountRequest.getEmail(), + accountRequest.getInstitute(), AccountRequestStatus.APPROVED, accountRequest.getComments()); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, accountRequest.getId().toString()}; + action = getAction(requestBody, params); + result = getJsonResult(action, 200); + data = (AccountRequestData) result.getOutput(); + + assertEquals(accountRequest.getName(), data.getName()); + assertEquals(accountRequest.getEmail(), data.getEmail()); + assertEquals(accountRequest.getInstitute(), data.getInstitute()); + assertEquals(AccountRequestStatus.APPROVED, data.getStatus()); + assertEquals(accountRequest.getComments(), data.getComments()); + verifyNumberOfEmailsSent(1); + + ______TS("already registered account request has no email sent when approved"); + accountRequest = logic.createAccountRequestWithTransaction("name", "email@email.com", + "institute", AccountRequestStatus.REGISTERED, "comments"); + requestBody = new AccountRequestUpdateRequest(name, email, institute, AccountRequestStatus.APPROVED, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, accountRequest.getId().toString()}; + + action = getAction(requestBody, params); + result = getJsonResult(action, 200); + data = (AccountRequestData) result.getOutput(); + + assertEquals(name, data.getName()); + assertEquals(email, data.getEmail()); + assertEquals(institute, data.getInstitute()); + assertEquals(AccountRequestStatus.REGISTERED, data.getStatus()); + assertEquals(comments, data.getComments()); + verifyNumberOfEmailsSent(0); + + ______TS("email with existing account throws exception"); + Account account = logic.createAccountWithTransaction(getTypicalAccount()); + accountRequest = logic.createAccountRequestWithTransaction("name", account.getEmail(), + "institute", AccountRequestStatus.PENDING, "comments"); + requestBody = new AccountRequestUpdateRequest(name, email, institute, AccountRequestStatus.APPROVED, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, accountRequest.getId().toString()}; + + InvalidOperationException ipe = verifyInvalidOperation(requestBody, params); + + assertEquals(String.format("An account with email %s already exists. " + + "Please reject or delete the account request instead.", account.getEmail()), ipe.getMessage()); + + ______TS("non-existent but valid uuid"); + requestBody = new AccountRequestUpdateRequest("name", "email", + "institute", AccountRequestStatus.PENDING, "comments"); + String validUuid = UUID.randomUUID().toString(); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, validUuid}; + + EntityNotFoundException enfe = verifyEntityNotFound(requestBody, params); + + assertEquals(String.format("Account request with id = %s not found", validUuid), enfe.getMessage()); + + ______TS("invalid uuid"); + requestBody = new AccountRequestUpdateRequest("name", "email", + "institute", AccountRequestStatus.PENDING, "comments"); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, "invalid"}; + + InvalidHttpParameterException ihpe = verifyHttpParameterFailure(requestBody, params); + + assertEquals("Expected UUID value for id parameter, but found: [invalid]", ihpe.getMessage()); + + ______TS("invalid email"); + accountRequest = logic.createAccountRequestWithTransaction("name", "email@email.com", + "institute", AccountRequestStatus.PENDING, "comments"); + id = accountRequest.getId(); + email = "newEmail"; + status = accountRequest.getStatus(); + + requestBody = new AccountRequestUpdateRequest(name, email, institute, status, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + InvalidHttpRequestBodyException ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals(getPopulatedErrorMessage(FieldValidator.EMAIL_ERROR_MESSAGE, email, + FieldValidator.EMAIL_FIELD_NAME, FieldValidator.REASON_INCORRECT_FORMAT, FieldValidator.EMAIL_MAX_LENGTH), + ihrbe.getMessage()); + + ______TS("invalid name alphanumeric"); + name = "@$@#$#@#@$#@$"; + email = "newEmail@email.com"; + + requestBody = new AccountRequestUpdateRequest(name, email, institute, status, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals(getPopulatedErrorMessage(FieldValidator.INVALID_NAME_ERROR_MESSAGE, name, + FieldValidator.PERSON_NAME_FIELD_NAME, FieldValidator.REASON_START_WITH_NON_ALPHANUMERIC_CHAR), + ihrbe.getMessage()); + + ______TS("invalid name too long"); + name = StringHelperExtension.generateStringOfLength(FieldValidator.PERSON_NAME_MAX_LENGTH + 1); + + requestBody = new AccountRequestUpdateRequest(name, email, institute, status, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals(getPopulatedErrorMessage(FieldValidator.SIZE_CAPPED_NON_EMPTY_STRING_ERROR_MESSAGE, name, + FieldValidator.PERSON_NAME_FIELD_NAME, FieldValidator.REASON_TOO_LONG, + FieldValidator.PERSON_NAME_MAX_LENGTH), ihrbe.getMessage()); + + ______TS("null email value"); + name = "newName"; + + requestBody = new AccountRequestUpdateRequest(name, null, institute, status, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals("email cannot be null", ihrbe.getMessage()); + + ______TS("null name value"); + requestBody = new AccountRequestUpdateRequest(null, email, institute, status, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals("name cannot be null", ihrbe.getMessage()); + + ______TS("null status value"); + requestBody = new AccountRequestUpdateRequest(name, email, institute, null, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals("status cannot be null", ihrbe.getMessage()); + + ______TS("null institute value"); + requestBody = new AccountRequestUpdateRequest(name, email, null, status, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals("institute cannot be null", ihrbe.getMessage()); + + ______TS("allow null comments in request"); + requestBody = new AccountRequestUpdateRequest(name, email, institute, status, null); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + action = getAction(requestBody, params); + result = getJsonResult(action, 200); + data = (AccountRequestData) result.getOutput(); + + assertEquals(name, data.getName()); + assertEquals(email, data.getEmail()); + assertEquals(institute, data.getInstitute()); + assertEquals(null, data.getComments()); + + ______TS("email with approved account request throws exception"); + logic.createAccountRequestWithTransaction("test", "test@email.com", + "institute", AccountRequestStatus.APPROVED, "comments"); + accountRequest = logic.createAccountRequestWithTransaction("test", "test@email.com", + "institute", AccountRequestStatus.PENDING, "comments"); + requestBody = new AccountRequestUpdateRequest(accountRequest.getName(), accountRequest.getEmail(), + accountRequest.getInstitute(), AccountRequestStatus.APPROVED, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, accountRequest.getId().toString()}; + + ipe = verifyInvalidOperation(requestBody, params); + + assertEquals(String.format("An account request with email %s has already been approved. " + + "Please reject or delete the account request instead.", accountRequest.getEmail()), ipe.getMessage()); + } + + @Override + @Test + protected void testAccessControl() throws InvalidParametersException, EntityAlreadyExistsException { + verifyOnlyAdminCanAccessWithTransaction(); + } + + @Override + @AfterMethod + protected void tearDown() { + HibernateUtil.beginTransaction(); + List accountRequests = logic.getAllAccountRequests(); + for (AccountRequest ar : accountRequests) { + logic.deleteAccountRequest(ar.getId()); + } + HibernateUtil.commitTransaction(); + } +} diff --git a/src/it/java/teammates/it/ui/webapi/UpdateFeedbackSessionLogsActionIT.java b/src/it/java/teammates/it/ui/webapi/UpdateFeedbackSessionLogsActionIT.java new file mode 100644 index 000000000000..b6f2a8b1f474 --- /dev/null +++ b/src/it/java/teammates/it/ui/webapi/UpdateFeedbackSessionLogsActionIT.java @@ -0,0 +1,236 @@ +package teammates.it.ui.webapi; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.FeedbackSessionLogEntry; +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.util.Const; +import teammates.common.util.HibernateUtil; +import teammates.common.util.TimeHelper; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.storage.sqlentity.Student; +import teammates.storage.sqlsearch.SearchManagerFactory; +import teammates.ui.webapi.UpdateFeedbackSessionLogsAction; + +/** + * SUT: {@link UpdateFeedbackSessionLogsAction}. + */ +public class UpdateFeedbackSessionLogsActionIT extends BaseActionIT { + + static final long COLLECTION_TIME_PERIOD = Const.STUDENT_ACTIVITY_LOGS_UPDATE_INTERVAL.toMinutes(); + static final long SPAM_FILTER = Const.STUDENT_ACTIVITY_LOGS_FILTER_WINDOW.toMillis(); + + Student student1InCourse1; + Student student2InCourse1; + Student student1InCourse3; + + Course course1; + Course course3; + + FeedbackSession session1InCourse1; + FeedbackSession session2InCourse1; + FeedbackSession session1InCourse3; + + Instant endTime; + Instant startTime; + + @Override + @BeforeMethod + protected void setUp() throws Exception { + super.setUp(); + persistDataBundle(typicalBundle); + HibernateUtil.flushSession(); + SearchManagerFactory.getStudentSearchManager().resetCollections(); + + endTime = TimeHelper.getInstantNearestQuarterHourBefore(Instant.now()); + startTime = endTime.minus(COLLECTION_TIME_PERIOD, ChronoUnit.MINUTES); + + course1 = typicalBundle.courses.get("course1"); + course3 = typicalBundle.courses.get("course3"); + + student1InCourse1 = typicalBundle.students.get("student1InCourse1"); + student2InCourse1 = typicalBundle.students.get("student2InCourse1"); + student1InCourse3 = typicalBundle.students.get("student1InCourse3"); + + session1InCourse1 = typicalBundle.feedbackSessions.get("session1InCourse1"); + session2InCourse1 = typicalBundle.feedbackSessions.get("session2InTypicalCourse"); + session1InCourse3 = typicalBundle.feedbackSessions.get("ongoingSession1InCourse3"); + + mockLogsProcessor.getOrderedFeedbackSessionLogs("", "", 0, 0, "").clear(); + } + + @Override + String getActionUri() { + return Const.CronJobURIs.AUTOMATED_FEEDBACK_SESSION_LOGS_PROCESSING; + } + + @Override + String getRequestMethod() { + return GET; + } + + @Test + @Override + protected void testExecute() { + ______TS("No spam all logs added"); + // Different Types + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(100).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.SUBMISSION.getLabel(), + startTime.plusSeconds(100).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.VIEW_RESULT.getLabel(), + startTime.plusSeconds(100).toEpochMilli()); + + // Different feedback sessions + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(200).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session2InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(200).toEpochMilli()); + + // Different Student + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(300).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student2InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(300).toEpochMilli()); + + // Different course + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(400).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course3.getId(), student1InCourse3.getId(), + session1InCourse3.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(400).toEpochMilli()); + + // Gap is larger than spam filter + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), startTime.toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusMillis(SPAM_FILTER + 1).toEpochMilli()); + + UpdateFeedbackSessionLogsAction action = getAction(); + getJsonResult(action); + + // method returns all logs regardless of params + List expected = mockLogsProcessor.getOrderedFeedbackSessionLogs("", "", 0, 0, ""); + List actual = logic.getOrderedFeedbackSessionLogs(course1.getId(), null, null, startTime, + endTime); + List actualCourse3 = logic.getOrderedFeedbackSessionLogs(course3.getId(), null, null, + startTime, endTime); + actual.addAll(actualCourse3); + assertTrue(isEqual(expected, actual)); + } + + @Test + protected void testExecute_recentLogsWithSpam_someLogsCreated() { + // Gap is smaller than spam filter + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), startTime.toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusMillis(SPAM_FILTER - 2).toEpochMilli()); + + // Filters multiple logs within one spam window + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusMillis(SPAM_FILTER - 1).toEpochMilli()); + + // Correctly adds new log after filtering + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusMillis(SPAM_FILTER + 1).toEpochMilli()); + + // Filters out spam in the new window + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusMillis(SPAM_FILTER + 2).toEpochMilli()); + + UpdateFeedbackSessionLogsAction action = getAction(); + action.execute(); + + List expected = new ArrayList<>(); + expected.add(new FeedbackSessionLogEntry(course1.getId(), student1InCourse1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.toEpochMilli())); + expected.add(new FeedbackSessionLogEntry(course1.getId(), student1InCourse1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusMillis(SPAM_FILTER + 1).toEpochMilli())); + + List actual = logic.getOrderedFeedbackSessionLogs(course1.getId(), null, null, startTime, + endTime); + assertTrue(isEqual(expected, actual)); + } + + @Test + protected void testExecute_badLogs_otherLogsCreated() { + UUID badUuid = UUID.fromString("00000000-0000-0000-0000-000000000000"); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(100).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(300).toEpochMilli()); + + // bad student id + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), badUuid, session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(200).toEpochMilli()); + + // bad session id + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), badUuid, + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(200).toEpochMilli()); + + UpdateFeedbackSessionLogsAction action = getAction(); + action.execute(); + + List expected = new ArrayList<>(); + expected.add(new FeedbackSessionLogEntry(course1.getId(), student1InCourse1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(100).toEpochMilli())); + expected.add(new FeedbackSessionLogEntry(course1.getId(), student1InCourse1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(300).toEpochMilli())); + + List actual = logic.getOrderedFeedbackSessionLogs(course1.getId(), null, null, startTime, + endTime); + assertTrue(isEqual(expected, actual)); + } + + @Test + @Override + protected void testAccessControl() throws Exception { + Course course = typicalBundle.courses.get("course1"); + verifyOnlyAdminCanAccess(course); + } + + private Boolean isEqual(List expected, List actual) { + + assertEquals(expected.size(), actual.size()); + + for (int i = 0; i < expected.size(); i++) { + FeedbackSessionLogEntry expectedEntry = expected.get(i); + FeedbackSessionLog actualLog = actual.get(i); + + assertEquals(expectedEntry.getStudentId(), actualLog.getStudent().getId()); + + assertEquals(expectedEntry.getFeedbackSessionId(), actualLog.getFeedbackSession().getId()); + + assertEquals(expectedEntry.getFeedbackSessionLogType(), actualLog.getFeedbackSessionLogType().getLabel()); + + assertEquals(expectedEntry.getTimestamp(), actualLog.getTimestamp().toEpochMilli()); + } + + return true; + } +} diff --git a/src/it/resources/data/DataBundleLogicIT.json b/src/it/resources/data/DataBundleLogicIT.json index 49e7cdec9930..371a87c963b0 100644 --- a/src/it/resources/data/DataBundleLogicIT.json +++ b/src/it/resources/data/DataBundleLogicIT.json @@ -19,6 +19,8 @@ "name": "Instructor 1", "email": "instr1@teammates.tmt", "institute": "TEAMMATES Test Institute 1", + "status": "REGISTERED", + "comments": "These are some comments.", "registeredAt": "2015-02-14T00:00:00Z" } }, diff --git a/src/it/resources/data/typicalDataBundle.json b/src/it/resources/data/typicalDataBundle.json index 2372b2fa35f2..a04d0ef7193f 100644 --- a/src/it/resources/data/typicalDataBundle.json +++ b/src/it/resources/data/typicalDataBundle.json @@ -73,14 +73,17 @@ "name": "Instructor 1", "email": "instr1@teammates.tmt", "institute": "TEAMMATES Test Institute 1", - "registeredAt": "2010-02-14T00:00:00Z" + "registeredAt": "2010-02-14T00:00:00Z", + "createdAt": "2011-02-01T00:00:00Z" }, "instructor2": { "id": "00000000-0000-4000-8000-000000000102", "name": "Instructor 2", "email": "instr2@teammates.tmt", "institute": "TEAMMATES Test Institute 1", - "registeredAt": "2015-02-14T00:00:00Z" + "registeredAt": "2015-02-14T00:00:00Z", + "comments": "Comments for account request from instructor2", + "status": "REGISTERED" }, "instructor3": { "name": "Instructor 3 of CourseNoRegister", @@ -1363,5 +1366,84 @@ "id": "00000000-0000-4000-8000-000000001101" } } + }, + "feedbackSessionLogs": { + "student1Session1Log1": { + "id": "00000000-0000-4000-8000-000000001301", + "student": { + "id" : "00000000-0000-4000-8000-000000000601" + }, + "feedbackSession": { + "id" : "00000000-0000-4000-8000-000000000701" + }, + "feedbackSessionLogType": "ACCESS", + "timestamp": "2012-01-01T12:00:00Z" + }, + "student1Session2Log1": { + "id": "00000000-0000-4000-8000-000000001302", + "student": { + "id" : "00000000-0000-4000-8000-000000000601" + }, + "feedbackSession": { + "id" : "00000000-0000-4000-8000-000000000702" + }, + "feedbackSessionLogType": "ACCESS", + "timestamp": "2012-01-01T12:00:01Z" + }, + "student1Session2Log2": { + "id": "00000000-0000-4000-8000-000000001303", + "student": { + "id" : "00000000-0000-4000-8000-000000000601" + }, + "feedbackSession": { + "id" : "00000000-0000-4000-8000-000000000702" + }, + "feedbackSessionLogType": "SUBMISSION", + "timestamp": "2012-01-01T12:00:02Z" + }, + "student2Session1Log1": { + "id": "00000000-0000-4000-8000-000000001304", + "student": { + "id" : "00000000-0000-4000-8000-000000000602" + }, + "feedbackSession": { + "id" : "00000000-0000-4000-8000-000000000701" + }, + "feedbackSessionLogType": "ACCESS", + "timestamp": "2012-01-01T12:00:03Z" + }, + "student2Session1Log2": { + "id": "00000000-0000-4000-8000-000000001305", + "student": { + "id" : "00000000-0000-4000-8000-000000000602" + }, + "feedbackSession": { + "id" : "00000000-0000-4000-8000-000000000701" + }, + "feedbackSessionLogType": "SUBMISSION", + "timestamp": "2012-01-01T12:00:04Z" + }, + "student1InAnotherCourse": { + "id": "00000000-0000-4000-8000-000000001306", + "student": { + "id" : "00000000-0000-4000-8000-000000000601" + }, + "feedbackSession": { + "id" : "00000000-0000-4000-8000-000000000707" + }, + "feedbackSessionLogType": "ACCESS", + "timestamp": "2012-01-01T12:00:05Z" + }, + "outOfRangeLog": { + "id": "00000000-0000-4000-8000-000000001307", + "student": { + "id" : "00000000-0000-4000-8000-000000000602" + }, + "feedbackSession": { + "id" : "00000000-0000-4000-8000-000000000701" + }, + "feedbackSessionLogType": "ACCESS", + "timestamp": "2010-01-01T00:00:00Z" + } } } diff --git a/src/lnp/java/teammates/lnp/cases/BaseLNPTestCase.java b/src/lnp/java/teammates/lnp/cases/BaseLNPTestCase.java index 10caef0599a3..369b0daa9a05 100644 --- a/src/lnp/java/teammates/lnp/cases/BaseLNPTestCase.java +++ b/src/lnp/java/teammates/lnp/cases/BaseLNPTestCase.java @@ -24,7 +24,6 @@ import org.apache.jmeter.save.SaveService; import org.apache.jmeter.util.JMeterUtils; import org.apache.jorphan.collections.HashTree; -import org.apache.jorphan.collections.ListedHashTree; import com.google.gson.Gson; import com.google.gson.JsonObject; @@ -69,7 +68,7 @@ public abstract class BaseLNPTestCase extends BaseTestCase { * Returns the JMeter test plan for the L&P test case. * @return A nested tree structure that consists of the various elements that are used in the JMeter test. */ - protected abstract ListedHashTree getLnpTestPlan(); + protected abstract HashTree getLnpTestPlan(); /** * Sets up the specification for this L&P test case. diff --git a/src/lnp/java/teammates/lnp/cases/FeedbackQuestionUpdateLNPTest.java b/src/lnp/java/teammates/lnp/cases/FeedbackQuestionUpdateLNPTest.java index 9c700be7b983..149351241a8c 100644 --- a/src/lnp/java/teammates/lnp/cases/FeedbackQuestionUpdateLNPTest.java +++ b/src/lnp/java/teammates/lnp/cases/FeedbackQuestionUpdateLNPTest.java @@ -289,8 +289,8 @@ private String getTestEndpoint() { } @Override - protected ListedHashTree getLnpTestPlan() { - ListedHashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); + protected HashTree getLnpTestPlan() { + HashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); HashTree threadGroup = testPlan.add( JMeterElements.threadGroup(NUM_INSTRUCTORS, RAMP_UP_PERIOD, 1)); diff --git a/src/lnp/java/teammates/lnp/cases/FeedbackSessionSubmitLNPTest.java b/src/lnp/java/teammates/lnp/cases/FeedbackSessionSubmitLNPTest.java index 7745a6050bb3..8e56ed7748f8 100644 --- a/src/lnp/java/teammates/lnp/cases/FeedbackSessionSubmitLNPTest.java +++ b/src/lnp/java/teammates/lnp/cases/FeedbackSessionSubmitLNPTest.java @@ -228,8 +228,8 @@ private Map getRequestHeaders() { } @Override - protected ListedHashTree getLnpTestPlan() { - ListedHashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); + protected HashTree getLnpTestPlan() { + HashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); HashTree threadGroup = testPlan.add( JMeterElements.threadGroup(NUMBER_OF_USER_ACCOUNTS, RAMP_UP_PERIOD, 1)); threadGroup.add(JMeterElements.csvDataSet(getPathToTestDataFile(getCsvConfigPath()))); diff --git a/src/lnp/java/teammates/lnp/cases/FeedbackSessionViewLNPTest.java b/src/lnp/java/teammates/lnp/cases/FeedbackSessionViewLNPTest.java index 4462cfef5532..fd81114c6182 100644 --- a/src/lnp/java/teammates/lnp/cases/FeedbackSessionViewLNPTest.java +++ b/src/lnp/java/teammates/lnp/cases/FeedbackSessionViewLNPTest.java @@ -203,8 +203,8 @@ public List> generateCsvData() { } @Override - protected ListedHashTree getLnpTestPlan() { - ListedHashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); + protected HashTree getLnpTestPlan() { + HashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); HashTree threadGroup = testPlan.add( JMeterElements.threadGroup(NUMBER_OF_USER_ACCOUNTS, RAMP_UP_PERIOD, 1)); threadGroup.add(JMeterElements.csvDataSet(getPathToTestDataFile(getCsvConfigPath()))); diff --git a/src/lnp/java/teammates/lnp/cases/InstructorCourseUpdateLNPTest.java b/src/lnp/java/teammates/lnp/cases/InstructorCourseUpdateLNPTest.java index 141107c55639..7d3ccba40390 100644 --- a/src/lnp/java/teammates/lnp/cases/InstructorCourseUpdateLNPTest.java +++ b/src/lnp/java/teammates/lnp/cases/InstructorCourseUpdateLNPTest.java @@ -160,8 +160,8 @@ private String getTestEndpoint() { } @Override - protected ListedHashTree getLnpTestPlan() { - ListedHashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); + protected HashTree getLnpTestPlan() { + HashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); HashTree threadGroup = testPlan.add( JMeterElements.threadGroup(NUM_INSTRUCTORS, RAMP_UP_PERIOD, 1)); diff --git a/src/lnp/java/teammates/lnp/cases/InstructorSessionResultLNPTest.java b/src/lnp/java/teammates/lnp/cases/InstructorSessionResultLNPTest.java index abf846a6d8a1..91b57f3f2635 100644 --- a/src/lnp/java/teammates/lnp/cases/InstructorSessionResultLNPTest.java +++ b/src/lnp/java/teammates/lnp/cases/InstructorSessionResultLNPTest.java @@ -250,8 +250,8 @@ public List> generateCsvData() { } @Override - protected ListedHashTree getLnpTestPlan() { - ListedHashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); + protected HashTree getLnpTestPlan() { + HashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); HashTree threadGroup = testPlan.add( JMeterElements.threadGroup(1, RAMP_UP_PERIOD, 1)); diff --git a/src/lnp/java/teammates/lnp/cases/InstructorStudentCascadingUpdateLNPTest.java b/src/lnp/java/teammates/lnp/cases/InstructorStudentCascadingUpdateLNPTest.java index 935252212262..d644934aa330 100644 --- a/src/lnp/java/teammates/lnp/cases/InstructorStudentCascadingUpdateLNPTest.java +++ b/src/lnp/java/teammates/lnp/cases/InstructorStudentCascadingUpdateLNPTest.java @@ -266,8 +266,8 @@ protected void createCsvConfigDataFile() throws IOException { } @Override - protected ListedHashTree getLnpTestPlan() { - ListedHashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); + protected HashTree getLnpTestPlan() { + HashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); HashTree threadGroup = testPlan.add( JMeterElements.threadGroup(NUM_INSTRUCTORS, RAMP_UP_PERIOD, 1)); diff --git a/src/lnp/java/teammates/lnp/cases/InstructorStudentEnrollmentLNPTest.java b/src/lnp/java/teammates/lnp/cases/InstructorStudentEnrollmentLNPTest.java index 1801a7929b10..2d53db4a9be8 100644 --- a/src/lnp/java/teammates/lnp/cases/InstructorStudentEnrollmentLNPTest.java +++ b/src/lnp/java/teammates/lnp/cases/InstructorStudentEnrollmentLNPTest.java @@ -144,8 +144,8 @@ private String getTestEndpoint() { } @Override - protected ListedHashTree getLnpTestPlan() { - ListedHashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); + protected HashTree getLnpTestPlan() { + HashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); HashTree threadGroup = testPlan.add( JMeterElements.threadGroup(NUM_INSTRUCTORS, RAMP_UP_PERIOD, 1)); diff --git a/src/lnp/java/teammates/lnp/cases/InstructorUpdateLNPTest.java b/src/lnp/java/teammates/lnp/cases/InstructorUpdateLNPTest.java index 66cb189dc47f..ce7d6f770a2a 100644 --- a/src/lnp/java/teammates/lnp/cases/InstructorUpdateLNPTest.java +++ b/src/lnp/java/teammates/lnp/cases/InstructorUpdateLNPTest.java @@ -287,8 +287,8 @@ private String getTestEndpoint() { } @Override - protected ListedHashTree getLnpTestPlan() { - ListedHashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); + protected HashTree getLnpTestPlan() { + HashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); HashTree threadGroup = testPlan.add( JMeterElements.threadGroup(NUM_INSTRUCTORS, RAMP_UP_PERIOD, 1)); diff --git a/src/lnp/java/teammates/lnp/cases/StudentEmailUpdateLNPTest.java b/src/lnp/java/teammates/lnp/cases/StudentEmailUpdateLNPTest.java index dcd2efa4aa96..d714c9eef6fd 100644 --- a/src/lnp/java/teammates/lnp/cases/StudentEmailUpdateLNPTest.java +++ b/src/lnp/java/teammates/lnp/cases/StudentEmailUpdateLNPTest.java @@ -260,8 +260,8 @@ private String getTestEndpoint() { } @Override - protected ListedHashTree getLnpTestPlan() { - ListedHashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); + protected HashTree getLnpTestPlan() { + HashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); HashTree threadGroup = testPlan.add( JMeterElements.threadGroup(NUM_INSTRUCTORS, RAMP_UP_PERIOD, 1)); diff --git a/src/lnp/java/teammates/lnp/cases/StudentSectionUpdateLNPTest.java b/src/lnp/java/teammates/lnp/cases/StudentSectionUpdateLNPTest.java index 27cf4767e0f3..672c57cc8265 100644 --- a/src/lnp/java/teammates/lnp/cases/StudentSectionUpdateLNPTest.java +++ b/src/lnp/java/teammates/lnp/cases/StudentSectionUpdateLNPTest.java @@ -260,8 +260,8 @@ private String getTestEndpoint() { } @Override - protected ListedHashTree getLnpTestPlan() { - ListedHashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); + protected HashTree getLnpTestPlan() { + HashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); HashTree threadGroup = testPlan.add( JMeterElements.threadGroup(NUM_INSTRUCTORS, RAMP_UP_PERIOD, 1)); diff --git a/src/lnp/java/teammates/lnp/cases/StudentTeamUpdateLNPTest.java b/src/lnp/java/teammates/lnp/cases/StudentTeamUpdateLNPTest.java index 1545aa24ac72..8c1b86ed8f93 100644 --- a/src/lnp/java/teammates/lnp/cases/StudentTeamUpdateLNPTest.java +++ b/src/lnp/java/teammates/lnp/cases/StudentTeamUpdateLNPTest.java @@ -260,8 +260,8 @@ private String getTestEndpoint() { } @Override - protected ListedHashTree getLnpTestPlan() { - ListedHashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); + protected HashTree getLnpTestPlan() { + HashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); HashTree threadGroup = testPlan.add( JMeterElements.threadGroup(NUM_INSTRUCTORS, RAMP_UP_PERIOD, 1)); diff --git a/src/lnp/java/teammates/lnp/sql/BaseLNPTestCase.java b/src/lnp/java/teammates/lnp/sql/BaseLNPTestCase.java new file mode 100644 index 000000000000..94cb0eb698b8 --- /dev/null +++ b/src/lnp/java/teammates/lnp/sql/BaseLNPTestCase.java @@ -0,0 +1,361 @@ +package teammates.lnp.sql; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.List; +import java.util.StringJoiner; + +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.jmeter.engine.StandardJMeterEngine; +import org.apache.jmeter.report.config.ConfigurationException; +import org.apache.jmeter.report.dashboard.GenerationException; +import org.apache.jmeter.report.dashboard.ReportGenerator; +import org.apache.jmeter.reporters.ResultCollector; +import org.apache.jmeter.reporters.Summariser; +import org.apache.jmeter.save.SaveService; +import org.apache.jmeter.util.JMeterUtils; +import org.apache.jorphan.collections.HashTree; +import org.apache.jorphan.collections.ListedHashTree; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.stream.JsonReader; + +import teammates.common.datatransfer.SqlDataBundle; +import teammates.common.exception.HttpRequestFailedException; +import teammates.common.util.JsonUtils; +import teammates.common.util.Logger; +import teammates.lnp.util.BackDoor; +import teammates.lnp.util.LNPResultsStatistics; +import teammates.lnp.util.LNPSpecification; +import teammates.lnp.util.LNPSqlTestData; +import teammates.lnp.util.TestProperties; +import teammates.test.BaseTestCase; +import teammates.test.FileHelper; + +/** + * Base class for all L&P test cases. + */ +public abstract class BaseLNPTestCase extends BaseTestCase { + + static final String GET = HttpGet.METHOD_NAME; + static final String POST = HttpPost.METHOD_NAME; + static final String PUT = HttpPut.METHOD_NAME; + static final String DELETE = HttpDelete.METHOD_NAME; + + private static final Logger log = Logger.getLogger(); + + private static final int RESULT_COUNT = 3; + + final BackDoor backdoor = BackDoor.getInstance(); + String timeStamp; + LNPSpecification specification; + + /** + * Returns the test data used for the current test. + */ + protected abstract LNPSqlTestData getTestData(); + + /** + * Returns the JMeter test plan for the L&P test case. + * @return A nested tree structure that consists of the various elements that are used in the JMeter test. + */ + protected abstract ListedHashTree getLnpTestPlan(); + + /** + * Sets up the specification for this L&P test case. + */ + protected abstract void setupSpecification(); + + /** + * Returns the path to the generated JSON data bundle file. + */ + protected String getJsonDataPath() { + return "/" + getClass().getSimpleName() + timeStamp + ".json"; + } + + /** + * Returns the path to the generated JMeter CSV config file. + */ + protected String getCsvConfigPath() { + return "/" + getClass().getSimpleName() + "Config" + timeStamp + ".csv"; + } + + /** + * Returns the path to the generated JTL test results file. + */ + protected String getJtlResultsPath() { + return "/" + getClass().getSimpleName() + timeStamp + ".jtl"; + } + + @Override + protected String getTestDataFolder() { + return TestProperties.LNP_TEST_DATA_FOLDER; + } + + /** + * Returns the path to the data file, relative to the project root directory. + */ + protected String getPathToTestDataFile(String fileName) { + return getTestDataFolder() + fileName; + } + + /** + * Returns the path to the JSON test results statistics file, relative to the project root directory. + */ + private String getPathToTestStatisticsResultsFile() { + return String.format("%s/%sStatistics%s.json", TestProperties.LNP_TEST_RESULTS_FOLDER, + this.getClass().getSimpleName(), this.timeStamp); + } + + String createFileAndDirectory(String directory, String fileName) throws IOException { + File dir = new File(directory); + if (!dir.exists()) { + dir.mkdir(); + } + + String pathToFile = directory + fileName; + File file = new File(pathToFile); + + // Write data to the file; overwrite if it already exists + if (file.exists()) { + file.delete(); + } + file.createNewFile(); + return pathToFile; + } + + /** + * Creates the JSON data and writes it to the file specified by {@link #getJsonDataPath()}. + */ + void createJsonDataFile(LNPSqlTestData testData) throws IOException { + SqlDataBundle jsonData = testData.generateJsonData(); + + String pathToResultFile = createFileAndDirectory(TestProperties.LNP_TEST_DATA_FOLDER, getJsonDataPath()); + try (BufferedWriter bw = Files.newBufferedWriter(Paths.get(pathToResultFile))) { + bw.write(JsonUtils.toJson(jsonData, SqlDataBundle.class)); + bw.flush(); + } + } + + /** + * Creates the CSV data and writes it to the file specified by {@link #getCsvConfigPath()}. + */ + private void createCsvConfigDataFile(LNPSqlTestData testData) throws IOException { + List headers = testData.generateCsvHeaders(); + List> valuesList = testData.generateCsvData(); + + String pathToCsvFile = createFileAndDirectory(TestProperties.LNP_TEST_DATA_FOLDER, getCsvConfigPath()); + try (BufferedWriter bw = Files.newBufferedWriter(Paths.get(pathToCsvFile))) { + // Write headers and data to the CSV file + bw.write(convertToCsv(headers)); + + for (List values : valuesList) { + bw.write(convertToCsv(values)); + } + + bw.flush(); + } + } + + /** + * Converts the list of {@code values} to a CSV row. + * @return A single string containing {@code values} separated by pipelines and ending with newline. + */ + String convertToCsv(List values) { + StringJoiner csvRow = new StringJoiner("|", "", "\n"); + for (String value : values) { + csvRow.add(value); + } + return csvRow.toString(); + } + + /** + * Returns the L&P test results statistics. + * @return The initialized result statistics from the L&P test results. + * @throws IOException if there is an error when loading the result file. + */ + private LNPResultsStatistics getResultsStatistics() throws IOException { + Gson gson = new Gson(); + JsonReader reader = new JsonReader(Files.newBufferedReader(Paths.get(getPathToTestStatisticsResultsFile()))); + JsonObject jsonObject = gson.fromJson(reader, JsonObject.class); + + JsonObject endpointStats = jsonObject.getAsJsonObject("HTTP Request Sampler"); + return gson.fromJson(endpointStats, LNPResultsStatistics.class); + } + + /** + * Renames the default results statistics file to the name of the test. + */ + private void renameStatisticsFile() { + File defaultFile = new File(TestProperties.LNP_TEST_RESULTS_FOLDER + "/statistics.json"); + File lnpStatisticsFile = new File(getPathToTestStatisticsResultsFile()); + + if (lnpStatisticsFile.exists()) { + lnpStatisticsFile.delete(); + } + if (!defaultFile.renameTo(lnpStatisticsFile)) { + log.warning("Failed to rename generated statistics.json file."); + } + } + + /** + * Setup and load the JMeter configuration and property files to run the Jmeter test. + * @throws IOException if the save service properties file cannot be loaded. + */ + private void setJmeterProperties() throws IOException { + JMeterUtils.loadJMeterProperties(TestProperties.JMETER_PROPERTIES_PATH); + JMeterUtils.setJMeterHome(TestProperties.JMETER_HOME); + JMeterUtils.initLocale(); + SaveService.loadProperties(); + } + + /** + * Creates the JSON test data and CSV config data files for the performance test from {@code testData}. + */ + protected void createTestData() throws IOException, HttpRequestFailedException { + LNPSqlTestData testData = getTestData(); + createJsonDataFile(testData); + persistTestData(); + createCsvConfigDataFile(testData); + } + + /** + * Creates the entities in the database from the JSON data file. + */ + protected void persistTestData() throws IOException, HttpRequestFailedException { + SqlDataBundle dataBundle = loadSqlDataBundle(getJsonDataPath()); + SqlDataBundle responseBody = backdoor.removeAndRestoreSqlDataBundle(dataBundle); + + String pathToResultFile = createFileAndDirectory(TestProperties.LNP_TEST_DATA_FOLDER, getJsonDataPath()); + String jsonValue = JsonUtils.toJson(responseBody, SqlDataBundle.class); + FileHelper.saveFile(pathToResultFile, jsonValue); + } + + /** + * Display the L&P results on the console. + */ + protected void displayLnpResults() throws IOException { + LNPResultsStatistics resultsStats = getResultsStatistics(); + + resultsStats.displayLnpResultsStatistics(); + specification.verifyLnpTestSuccess(resultsStats); + } + + /** + * Runs the JMeter test. + * @param shouldCreateJmxFile true if the generated test plan should be saved to a `.jmx` file which + * can be opened in the JMeter GUI, and false otherwise. + */ + protected void runJmeter(boolean shouldCreateJmxFile) throws IOException { + StandardJMeterEngine jmeter = new StandardJMeterEngine(); + setJmeterProperties(); + + HashTree testPlan = getLnpTestPlan(); + + if (shouldCreateJmxFile) { + String pathToConfigFile = createFileAndDirectory( + TestProperties.LNP_TEST_CONFIG_FOLDER, "/" + getClass().getSimpleName() + ".jmx"); + SaveService.saveTree(testPlan, Files.newOutputStream(Paths.get(pathToConfigFile))); + } + + // Add result collector to the test plan for generating results file + Summariser summariser = null; + String summariserName = JMeterUtils.getPropDefault("summariser.name", "summary"); + if (summariserName.length() > 0) { + summariser = new Summariser(summariserName); + } + + String resultsFile = createFileAndDirectory(TestProperties.LNP_TEST_RESULTS_FOLDER, getJtlResultsPath()); + ResultCollector resultCollector = new ResultCollector(summariser); + resultCollector.setFilename(resultsFile); + testPlan.add(testPlan.getArray()[0], resultCollector); + + // Run Jmeter Test + jmeter.configure(testPlan); + jmeter.run(); + + try { + ReportGenerator reportGenerator = new ReportGenerator(resultsFile, null); + reportGenerator.generate(); + } catch (ConfigurationException | GenerationException e) { + log.warning(e.getMessage()); + } + + renameStatisticsFile(); + } + + /** + * Deletes the data that was created in the database from the JSON data file. + */ + protected void deleteTestData() { + SqlDataBundle dataBundle = loadSqlDataBundle(getJsonDataPath()); + backdoor.removeSqlDataBundle(dataBundle); + } + + /** + * Deletes the JSON and CSV data files that were created. + */ + protected void deleteDataFiles() throws IOException { + String pathToJsonFile = getPathToTestDataFile(getJsonDataPath()); + String pathToCsvFile = getPathToTestDataFile(getCsvConfigPath()); + + Files.delete(Paths.get(pathToJsonFile)); + Files.delete(Paths.get(pathToCsvFile)); + } + + /** + * Deletes the oldest excess result .jtl file and the statistics file, if there are more than RESULT_COUNT. + */ + protected void cleanupResults() throws IOException { + File[] fileList = new File(TestProperties.LNP_TEST_RESULTS_FOLDER) + .listFiles((d, s) -> { + return s.contains(this.getClass().getSimpleName()); + }); + if (fileList == null) { + fileList = new File[] {}; + } + Arrays.sort(fileList, (a, b) -> { + return b.getName().compareTo(a.getName()); + }); + + int jtlCounter = 0; + int statisticsCounter = 0; + for (File file : fileList) { + if (file.getName().contains("Statistics")) { + statisticsCounter++; + if (statisticsCounter > RESULT_COUNT) { + Files.delete(file.toPath()); + } + } else { + jtlCounter++; + if (jtlCounter > RESULT_COUNT) { + Files.delete(file.toPath()); + } + } + } + } + + /** + * Sanitize the string to be CSV-safe string. + */ + protected String sanitizeForCsv(String originalString) { + return String.format("\"%s\"", originalString.replace(System.lineSeparator(), "").replace("\"", "\"\"")); + } + + /** + * Generates timestamp for generated statistics/CSV files in order to prevent concurrency issues. + */ + protected void generateTimeStamp() { + this.timeStamp = ZonedDateTime.now().format(DateTimeFormatter.ofPattern("_uuuuMMddHHmmss")); + } +} diff --git a/src/lnp/java/teammates/lnp/sql/InstructorCourseUpdateLNPTest.java b/src/lnp/java/teammates/lnp/sql/InstructorCourseUpdateLNPTest.java new file mode 100644 index 000000000000..6196fd1f916f --- /dev/null +++ b/src/lnp/java/teammates/lnp/sql/InstructorCourseUpdateLNPTest.java @@ -0,0 +1,216 @@ +package teammates.lnp.sql; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.jmeter.protocol.http.control.HeaderManager; +import org.apache.jorphan.collections.HashTree; +import org.apache.jorphan.collections.ListedHashTree; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.InstructorPermissionRole; +import teammates.common.datatransfer.InstructorPrivileges; +import teammates.common.datatransfer.SqlDataBundle; +import teammates.common.exception.HttpRequestFailedException; +import teammates.common.util.Const; +import teammates.common.util.JsonUtils; +import teammates.lnp.util.JMeterElements; +import teammates.lnp.util.LNPSpecification; +import teammates.lnp.util.LNPSqlTestData; +import teammates.storage.sqlentity.Account; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.Instructor; +import teammates.ui.request.CourseUpdateRequest; + +/** + * L&P Test Case for course update cascade API. + */ +public class InstructorCourseUpdateLNPTest extends BaseLNPTestCase { + private static final int NUM_INSTRUCTORS = 1; + private static final int RAMP_UP_PERIOD = NUM_INSTRUCTORS * 2; + + private static final int NUM_FEEDBACK_SESSIONS = 500; + + private static final String COURSE_ID = "TestData.CS101"; + private static final String COURSE_NAME = "LnPCourse"; + private static final String COURSE_TIME_ZONE = "UTC"; + private static final String COURSE_INSTITUTE = "LnpInstitute"; + + private static final String ACCOUNT_NAME = "LnpAccount"; + + private static final String UPDATE_COURSE_NAME = "updatedCourse"; + private static final String UPDATE_COURSE_TIME_ZONE = "GMT"; + + private static final String INSTRUCTOR_ID = "LnPInstructor_id"; + private static final String INSTRUCTOR_NAME = "LnPInstructor"; + private static final String INSTRUCTOR_EMAIL = "tmms.test@gmail.tmt"; + + private static final String FEEDBACK_SESSION_NAME = "Test Feedback Session"; + + private static final double ERROR_RATE_LIMIT = 0.01; + private static final double MEAN_RESP_TIME_LIMIT = 10; + + @Override + protected LNPSqlTestData getTestData() { + Account instructorAccount = new Account(INSTRUCTOR_ID, ACCOUNT_NAME, INSTRUCTOR_EMAIL); + Course instructorCourse = new Course(COURSE_ID, COURSE_NAME, COURSE_TIME_ZONE, COURSE_INSTITUTE); + return new LNPSqlTestData() { + @Override + protected Map generateCourses() { + Map courses = new HashMap<>(); + + courses.put(COURSE_NAME, instructorCourse); + + return courses; + } + + @Override + protected Map generateAccounts() { + Map accounts = new HashMap<>(); + + accounts.put(ACCOUNT_NAME, instructorAccount); + + return accounts; + } + + @Override + protected Map generateInstructors() { + Map instructors = new HashMap<>(); + + Instructor instructor = new Instructor( + instructorCourse, INSTRUCTOR_NAME, INSTRUCTOR_EMAIL, + true, "Co-owner", InstructorPermissionRole.INSTRUCTOR_PERMISSION_ROLE_COOWNER, + new InstructorPrivileges(Const.InstructorPermissionRoleNames.INSTRUCTOR_PERMISSION_ROLE_COOWNER)); + + instructor.setAccount(instructorAccount); + instructors.put(INSTRUCTOR_NAME, instructor); + + return instructors; + } + + @Override + protected Map generateFeedbackSessions() { + Map feedbackSessions = new LinkedHashMap<>(); + + for (int i = 1; i <= NUM_FEEDBACK_SESSIONS; i++) { + Instant now = Instant.now(); + FeedbackSession session = new FeedbackSession(FEEDBACK_SESSION_NAME + " " + i, + instructorCourse, INSTRUCTOR_EMAIL, "", + now.plus(Duration.ofMinutes(1)), now.plus(Duration.ofDays(1)), + now, now.plus(Duration.ofDays(2)), null, false, false, false); + + feedbackSessions.put(FEEDBACK_SESSION_NAME + " " + i, session); + } + + return feedbackSessions; + } + + @Override + public List generateCsvHeaders() { + List headers = new ArrayList<>(); + + headers.add("loginId"); + headers.add("courseId"); + headers.add("updateData"); + + return headers; + } + + @Override + public List> generateCsvData() { + SqlDataBundle dataBundle = loadSqlDataBundle(getJsonDataPath()); + List> csvData = new ArrayList<>(); + + dataBundle.instructors.forEach((key, instructor) -> { + List csvRow = new ArrayList<>(); + + csvRow.add(INSTRUCTOR_ID); + csvRow.add(COURSE_ID); + + CourseUpdateRequest courseUpdateRequest = new CourseUpdateRequest(); + courseUpdateRequest.setCourseName(UPDATE_COURSE_NAME); + courseUpdateRequest.setTimeZone(UPDATE_COURSE_TIME_ZONE); + + String updateData = sanitizeForCsv(JsonUtils.toJson(courseUpdateRequest)); + csvRow.add(updateData); + + csvData.add(csvRow); + }); + + return csvData; + } + }; + } + + private Map getRequestHeaders() { + Map headers = new HashMap<>(); + + headers.put(Const.HeaderNames.CSRF_TOKEN, "${csrfToken}"); + headers.put("Content-Type", "application/json"); + + return headers; + } + + private String getTestEndpoint() { + return Const.ResourceURIs.COURSE + "?courseid=${courseId}"; + } + + @Override + protected ListedHashTree getLnpTestPlan() { + ListedHashTree testPlan = new ListedHashTree(JMeterElements.testPlan()); + HashTree threadGroup = testPlan.add( + JMeterElements.threadGroup(NUM_INSTRUCTORS, RAMP_UP_PERIOD, 1)); + + threadGroup.add(JMeterElements.csvDataSet(getPathToTestDataFile(getCsvConfigPath()))); + threadGroup.add(JMeterElements.cookieManager()); + threadGroup.add(JMeterElements.defaultSampler()); + + threadGroup.add(JMeterElements.onceOnlyController()) + .add(JMeterElements.loginSampler()) + .add(JMeterElements.csrfExtractor("csrfToken")); + + // Add HTTP sampler for test endpoint + HeaderManager headerManager = JMeterElements.headerManager(getRequestHeaders()); + threadGroup.add(JMeterElements.httpSampler(getTestEndpoint(), PUT, "${updateData}")) + .add(headerManager); + + return testPlan; + } + + @Override + protected void setupSpecification() { + this.specification = LNPSpecification.builder() + .withErrorRateLimit(ERROR_RATE_LIMIT) + .withMeanRespTimeLimit(MEAN_RESP_TIME_LIMIT) + .build(); + } + + @BeforeClass + public void classSetup() throws IOException, HttpRequestFailedException { + generateTimeStamp(); + createTestData(); + setupSpecification(); + } + + @Test + public void runLnpTest() throws IOException { + runJmeter(false); + displayLnpResults(); + } + + @AfterClass + public void classTearDown() throws IOException { + deleteTestData(); + deleteDataFiles(); + cleanupResults(); + } +} diff --git a/src/lnp/java/teammates/lnp/sql/package-info.java b/src/lnp/java/teammates/lnp/sql/package-info.java new file mode 100644 index 000000000000..bf87f3a9fe8f --- /dev/null +++ b/src/lnp/java/teammates/lnp/sql/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains L&P test cases. + */ +package teammates.lnp.sql; diff --git a/src/lnp/java/teammates/lnp/util/LNPSpecification.java b/src/lnp/java/teammates/lnp/util/LNPSpecification.java index 2a0dd778e0b1..f0cb47c7b0b4 100644 --- a/src/lnp/java/teammates/lnp/util/LNPSpecification.java +++ b/src/lnp/java/teammates/lnp/util/LNPSpecification.java @@ -69,7 +69,7 @@ public static Builder builder() { /** * A builder class for {@link LNPSpecification}. */ - public static class Builder { + public static final class Builder { private LNPSpecification specification; diff --git a/src/lnp/java/teammates/lnp/util/LNPSqlTestData.java b/src/lnp/java/teammates/lnp/util/LNPSqlTestData.java new file mode 100644 index 000000000000..48aebf22f2df --- /dev/null +++ b/src/lnp/java/teammates/lnp/util/LNPSqlTestData.java @@ -0,0 +1,92 @@ +package teammates.lnp.util; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import teammates.common.datatransfer.SqlDataBundle; +import teammates.storage.sqlentity.Account; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackQuestion; +import teammates.storage.sqlentity.FeedbackResponse; +import teammates.storage.sqlentity.FeedbackResponseComment; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.Instructor; +import teammates.storage.sqlentity.Student; + +/** + * L&P test data generator. + */ +public abstract class LNPSqlTestData { + + // CHECKSTYLE.OFF:MissingJavadocMethod generator for different entities are self-explained by the method name + + protected Map generateAccounts() { + return new HashMap<>(); + } + + protected Map generateCourses() { + return new HashMap<>(); + } + + protected Map generateInstructors() { + return new HashMap<>(); + } + + protected Map generateStudents() { + return new HashMap<>(); + } + + protected Map generateFeedbackSessions() { + return new HashMap<>(); + } + + protected Map generateFeedbackQuestions() { + return new HashMap<>(); + } + + protected Map generateFeedbackResponses() { + return new HashMap<>(); + } + + protected Map generateFeedbackResponseComments() { + return new HashMap<>(); + } + + // CHECKSTYLE.ON:MissingJavadocMethod + + /** + * Returns a JSON data bundle containing the data relevant for the performance test. + */ + public SqlDataBundle generateJsonData() { + SqlDataBundle dataBundle = new SqlDataBundle(); + + dataBundle.accounts = generateAccounts(); + dataBundle.courses = generateCourses(); + dataBundle.instructors = generateInstructors(); + dataBundle.students = generateStudents(); + dataBundle.feedbackSessions = generateFeedbackSessions(); + dataBundle.feedbackQuestions = generateFeedbackQuestions(); + dataBundle.feedbackResponses = generateFeedbackResponses(); + dataBundle.feedbackResponseComments = generateFeedbackResponseComments(); + + return dataBundle; + } + + /** + * Returns list of header fields for the data in the CSV file to be generated. + * + *

Note that these header names should correspond to the variables used in the JMeter L&P test.

+ */ + public abstract List generateCsvHeaders(); + + /** + * Returns the data for the entries in the CSV file to be generated. + * The order of the field values for each entry should correspond to the order of headers specified + * in {@link #generateCsvHeaders()}. + * + * @return List of entries, which are made up of a list of field values. + */ + public abstract List> generateCsvData(); + +} diff --git a/src/lnp/java/teammates/lnp/util/TestProperties.java b/src/lnp/java/teammates/lnp/util/TestProperties.java index 91f7ef7f8319..932cad931db0 100644 --- a/src/lnp/java/teammates/lnp/util/TestProperties.java +++ b/src/lnp/java/teammates/lnp/util/TestProperties.java @@ -9,6 +9,7 @@ /** * Represents properties in test.properties file. */ +@SuppressWarnings("PMD.TestClassWithoutTestCases") public final class TestProperties { /** The directory where the L&P test data files are stored. */ diff --git a/src/main/appengine/app.template.yaml b/src/main/appengine/app.template.yaml index 212a7fc4b9fd..ad47c59733e7 100644 --- a/src/main/appengine/app.template.yaml +++ b/src/main/appengine/app.template.yaml @@ -3,7 +3,7 @@ ########################################################################### # App Engine Standard (Java 11) configuration -# Reference: https://cloud.google.com/appengine/docs/standard/java11/config/appref +# Reference: https://cloud.google.com/appengine/docs/standard/reference/app-yaml?tab=java ########################################################################### # Run/deploy with Java 11 runtime. @@ -30,7 +30,7 @@ instance_class: F1 handlers: # These are the handlers for static files. By specifying them as static files, requests pointing to these URLs will not add up to instance load. - # Reference: https://cloud.google.com/appengine/docs/standard/java11/config/appref#handlers_element + # Reference: https://cloud.google.com/appengine/docs/standard/reference/app-yaml?tab=java#handlers_element # Assets and front-end files - url: /assets @@ -79,7 +79,7 @@ handlers: # Defines policy for automatic scaling (instance class F*). # If the application uses instance class B*, this part is not relevant. -# Reference: https://cloud.google.com/appengine/docs/standard/java11/config/appref#scaling_elements +# Reference: https://cloud.google.com/appengine/docs/standard/reference/app-yaml?tab=java#scaling_elements automatic_scaling: # max_instances: 0 # min_instances: 0 @@ -92,7 +92,7 @@ automatic_scaling: target_throughput_utilization: 0.6 # Enable warmup request to reduce latency when new instances are created. -# Reference: https://cloud.google.com/appengine/docs/standard/java11/configuring-warmup-requests +# Reference: https://cloud.google.com/appengine/docs/standard/configuring-warmup-requests?tab=java inbound_services: - warmup diff --git a/src/main/appengine/cron.yaml b/src/main/appengine/cron.yaml index 07a94809826d..f544189852c5 100644 --- a/src/main/appengine/cron.yaml +++ b/src/main/appengine/cron.yaml @@ -1,4 +1,4 @@ -# Reference: https://cloud.google.com/appengine/docs/standard/java11/scheduling-jobs-with-cron-yaml +# Reference: https://cloud.google.com/appengine/docs/standard/scheduling-jobs-with-cron-yaml cron: - url: '/auto/feedbackSessionOpeningReminders' @@ -33,3 +33,7 @@ cron: schedule: 'every 5 minutes synchronized' timezone: 'Asia/Singapore' description: 'Compile severe logs and sends out email notifications.' +- url: '/auto/updateFeedbackSessionLogs' + schedule: 'every 15 minutes from 00:01 to 23:59' + timezone: 'Asia/Singapore' + description: 'Process feedback session activity logs from logging service and store in the database.' diff --git a/src/main/appengine/index.yaml b/src/main/appengine/index.yaml index db65f2b21b8d..33b16c66f749 100644 --- a/src/main/appengine/index.yaml +++ b/src/main/appengine/index.yaml @@ -93,3 +93,15 @@ indexes: name: targetUser - direction: asc name: endTime +- kind: Course + properties: + - direction: asc + name: isMigrated + - direction: asc + name: createdAt +- kind: Course + properties: + - direction: asc + name: isMigrated + - direction: desc + name: createdAt diff --git a/src/main/appengine/queue.yaml b/src/main/appengine/queue.yaml index 09d9b215e6ab..1521525a542b 100644 --- a/src/main/appengine/queue.yaml +++ b/src/main/appengine/queue.yaml @@ -1,4 +1,3 @@ -# Reference: https://cloud.google.com/appengine/docs/standard/java/config/queueref-yaml # Reference: https://cloud.google.com/tasks/docs/queue-yaml # Although the usage of queue.yaml is somewhat deprecated, we continue to use it for now # as it is more practical for us compared to managing task queue within code. diff --git a/src/main/docker/Dockerfile b/src/main/docker/Dockerfile index a4a5c7cd4f21..2cd5d7824708 100644 --- a/src/main/docker/Dockerfile +++ b/src/main/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM openjdk:11-jre +FROM amazoncorretto:11-alpine-jdk COPY dist . COPY WEB-INF WEB-INF diff --git a/src/main/java/teammates/common/datatransfer/AccountRequestStatus.java b/src/main/java/teammates/common/datatransfer/AccountRequestStatus.java new file mode 100644 index 000000000000..db80e7cb8303 --- /dev/null +++ b/src/main/java/teammates/common/datatransfer/AccountRequestStatus.java @@ -0,0 +1,27 @@ +package teammates.common.datatransfer; + +/** + * The status of an account request. + */ +public enum AccountRequestStatus { + + /** + * The account request has not yet been processed by the admin. + */ + PENDING, + + /** + * The account request has been rejected by the admin. + */ + REJECTED, + + /** + * The account request has been approved by the admin but the instructor has not created an account yet. + */ + APPROVED, + + /** + * The account request has been approved by the admin and the instructor has created an account. + */ + REGISTERED +} diff --git a/src/main/java/teammates/common/datatransfer/AttributesDeletionQuery.java b/src/main/java/teammates/common/datatransfer/AttributesDeletionQuery.java index b665397e875e..0b6afe1ffab9 100644 --- a/src/main/java/teammates/common/datatransfer/AttributesDeletionQuery.java +++ b/src/main/java/teammates/common/datatransfer/AttributesDeletionQuery.java @@ -74,7 +74,7 @@ public static Builder builder() { /** * Builder for {@link AttributesDeletionQuery}. */ - public static class Builder { + public static final class Builder { private static final String INVALID_COMBINATION = "Invalid combination"; diff --git a/src/main/java/teammates/common/datatransfer/CourseRoster.java b/src/main/java/teammates/common/datatransfer/CourseRoster.java index b47fd255b47d..793dfa7fd076 100644 --- a/src/main/java/teammates/common/datatransfer/CourseRoster.java +++ b/src/main/java/teammates/common/datatransfer/CourseRoster.java @@ -158,7 +158,7 @@ public ParticipantInfo getInfoForIdentifier(String identifier) { /** * Simple data transfer object containing the information of a participant. */ - public static class ParticipantInfo { + public static final class ParticipantInfo { private final String name; private final String teamName; diff --git a/src/main/java/teammates/common/datatransfer/FeedbackSessionLogEntry.java b/src/main/java/teammates/common/datatransfer/FeedbackSessionLogEntry.java index 1502071050e7..10608f179fb0 100644 --- a/src/main/java/teammates/common/datatransfer/FeedbackSessionLogEntry.java +++ b/src/main/java/teammates/common/datatransfer/FeedbackSessionLogEntry.java @@ -1,26 +1,57 @@ package teammates.common.datatransfer; +import java.util.UUID; + /** * Represents a log entry of a feedback session. */ -public class FeedbackSessionLogEntry { +public class FeedbackSessionLogEntry implements Comparable { + private final String courseId; + private final UUID studentId; private final String studentEmail; + private final UUID feedbackSessionId; private final String feedbackSessionName; private final String feedbackSessionLogType; private final long timestamp; - public FeedbackSessionLogEntry(String studentEmail, String feedbackSessionName, - String feedbackSessionLogType, long timestamp) { + public FeedbackSessionLogEntry(String courseId, String studentEmail, + String feedbackSessionName, String feedbackSessionLogType, long timestamp) { + this.courseId = courseId; + this.studentId = null; this.studentEmail = studentEmail; + this.feedbackSessionId = null; this.feedbackSessionName = feedbackSessionName; this.feedbackSessionLogType = feedbackSessionLogType; this.timestamp = timestamp; } + public FeedbackSessionLogEntry(String courseId, UUID studentId, UUID feedbackSessionId, + String feedbackSessionLogType, long timestamp) { + this.courseId = courseId; + this.studentId = studentId; + this.studentEmail = null; + this.feedbackSessionId = feedbackSessionId; + this.feedbackSessionName = null; + this.feedbackSessionLogType = feedbackSessionLogType; + this.timestamp = timestamp; + } + + public String getCourseId() { + return courseId; + } + + public UUID getStudentId() { + return studentId; + } + public String getStudentEmail() { return studentEmail; } + public UUID getFeedbackSessionId() { + return feedbackSessionId; + } + public String getFeedbackSessionName() { return feedbackSessionName; } @@ -32,4 +63,9 @@ public String getFeedbackSessionLogType() { public long getTimestamp() { return this.timestamp; } + + @Override + public int compareTo(FeedbackSessionLogEntry o) { + return Long.compare(this.getTimestamp(), o.getTimestamp()); + } } diff --git a/src/main/java/teammates/common/datatransfer/SqlCourseRoster.java b/src/main/java/teammates/common/datatransfer/SqlCourseRoster.java index 922b9d6c8c69..6cc462be0caa 100644 --- a/src/main/java/teammates/common/datatransfer/SqlCourseRoster.java +++ b/src/main/java/teammates/common/datatransfer/SqlCourseRoster.java @@ -158,7 +158,7 @@ public ParticipantInfo getInfoForIdentifier(String identifier) { /** * Simple data transfer object containing the information of a participant. */ - public static class ParticipantInfo { + public static final class ParticipantInfo { private final String name; private final String teamName; diff --git a/src/main/java/teammates/common/datatransfer/SqlDataBundle.java b/src/main/java/teammates/common/datatransfer/SqlDataBundle.java index d3a027b2775d..b411b4ea0949 100644 --- a/src/main/java/teammates/common/datatransfer/SqlDataBundle.java +++ b/src/main/java/teammates/common/datatransfer/SqlDataBundle.java @@ -11,6 +11,7 @@ import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.FeedbackResponseComment; import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; import teammates.storage.sqlentity.Instructor; import teammates.storage.sqlentity.Notification; import teammates.storage.sqlentity.ReadNotification; @@ -37,6 +38,7 @@ public class SqlDataBundle { public Map feedbackQuestions = new LinkedHashMap<>(); public Map feedbackResponses = new LinkedHashMap<>(); public Map feedbackResponseComments = new LinkedHashMap<>(); + public Map feedbackSessionLogs = new LinkedHashMap<>(); public Map notifications = new LinkedHashMap<>(); public Map readNotifications = new LinkedHashMap<>(); } diff --git a/src/main/java/teammates/common/datatransfer/TeamEvalResult.java b/src/main/java/teammates/common/datatransfer/TeamEvalResult.java index 428857aff18e..8d3358f76907 100644 --- a/src/main/java/teammates/common/datatransfer/TeamEvalResult.java +++ b/src/main/java/teammates/common/datatransfer/TeamEvalResult.java @@ -330,7 +330,7 @@ private static double averageColumn(double[][] arrayOfArrays, int columnIndex) { count++; } // omit calculation if no data points - double average = count == 0 ? NA : (double) (sum / count); + double average = count == 0 ? NA : (sum / count); String logMessage = "Average(" + values.toString().trim() + ") = " + average; log.fine(replaceMagicNumbers(logMessage)); diff --git a/src/main/java/teammates/common/datatransfer/attributes/AccountAttributes.java b/src/main/java/teammates/common/datatransfer/attributes/AccountAttributes.java index 00598d9dc715..5d9509a985fa 100644 --- a/src/main/java/teammates/common/datatransfer/attributes/AccountAttributes.java +++ b/src/main/java/teammates/common/datatransfer/attributes/AccountAttributes.java @@ -185,7 +185,7 @@ public static UpdateOptions.Builder updateOptionsBuilder(String googleId) { /** * A builder class for {@link AccountAttributes}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private AccountAttributes accountAttributes; @@ -221,7 +221,7 @@ public AccountAttributes build() { /** * Helper class to specify the fields to update in {@link AccountAttributes}. */ - public static class UpdateOptions { + public static final class UpdateOptions { private String googleId; private UpdateOption> readNotificationsOption = UpdateOption.empty(); @@ -249,7 +249,7 @@ public String toString() { /** * Builder class to build {@link UpdateOptions}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private Builder(String googleId) { super(new UpdateOptions(googleId)); diff --git a/src/main/java/teammates/common/datatransfer/attributes/AccountRequestAttributes.java b/src/main/java/teammates/common/datatransfer/attributes/AccountRequestAttributes.java index 70f97471d6f8..aa80974c49a6 100644 --- a/src/main/java/teammates/common/datatransfer/attributes/AccountRequestAttributes.java +++ b/src/main/java/teammates/common/datatransfer/attributes/AccountRequestAttributes.java @@ -15,7 +15,7 @@ * The data transfer object for {@link AccountRequest} entities. */ public final class AccountRequestAttributes extends EntityAttributes { - + private String id; private String email; private String name; private String institute; @@ -38,7 +38,7 @@ private AccountRequestAttributes(String email, String institute, String name) { public static AccountRequestAttributes valueOf(AccountRequest accountRequest) { AccountRequestAttributes accountRequestAttributes = new AccountRequestAttributes(accountRequest.getEmail(), accountRequest.getInstitute(), accountRequest.getName()); - + accountRequestAttributes.id = accountRequest.getId(); accountRequestAttributes.registrationKey = accountRequest.getRegistrationKey(); accountRequestAttributes.registeredAt = accountRequest.getRegisteredAt(); accountRequestAttributes.createdAt = accountRequest.getCreatedAt(); @@ -53,6 +53,10 @@ public static Builder builder(String email, String institute, String name) { return new Builder(email, institute, name); } + public String getId() { + return id; + } + public String getRegistrationKey() { return registrationKey; } @@ -163,7 +167,7 @@ public static UpdateOptions.Builder updateOptionsBuilder(String email, String in /** * A builder for {@link AccountRequestAttributes}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private final AccountRequestAttributes accountRequestAttributes; private Builder(String email, String institute, String name) { @@ -184,7 +188,7 @@ public AccountRequestAttributes build() { /** * Helper class to specify the fields to update in {@link AccountRequestAttributes}. */ - public static class UpdateOptions { + public static final class UpdateOptions { private String email; private String institute; @@ -218,7 +222,7 @@ public String toString() { /** * Builder class to build {@link UpdateOptions}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private Builder(String email, String institute) { super(new UpdateOptions(email, institute)); thisBuilder = this; diff --git a/src/main/java/teammates/common/datatransfer/attributes/CourseAttributes.java b/src/main/java/teammates/common/datatransfer/attributes/CourseAttributes.java index f28decd00a9e..13c13b52cf2b 100644 --- a/src/main/java/teammates/common/datatransfer/attributes/CourseAttributes.java +++ b/src/main/java/teammates/common/datatransfer/attributes/CourseAttributes.java @@ -213,7 +213,7 @@ public static UpdateOptions.Builder updateOptionsBuilder(String courseId) { /** * A builder for {@link CourseAttributes}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private final CourseAttributes courseAttributes; @@ -235,7 +235,7 @@ public CourseAttributes build() { /** * Helper class to specific the fields to update in {@link AccountAttributes}. */ - public static class UpdateOptions { + public static final class UpdateOptions { private String courseId; private UpdateOption nameOption = UpdateOption.empty(); @@ -267,7 +267,7 @@ public String toString() { /** * Builder class to build {@link UpdateOptions}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private Builder(String courseId) { super(new UpdateOptions(courseId)); diff --git a/src/main/java/teammates/common/datatransfer/attributes/DeadlineExtensionAttributes.java b/src/main/java/teammates/common/datatransfer/attributes/DeadlineExtensionAttributes.java index 3a96cea689bb..9d10a5046ab5 100644 --- a/src/main/java/teammates/common/datatransfer/attributes/DeadlineExtensionAttributes.java +++ b/src/main/java/teammates/common/datatransfer/attributes/DeadlineExtensionAttributes.java @@ -192,7 +192,7 @@ public static UpdateOptions.Builder updateOptionsBuilder(UpdateOptions updateOpt /** * A builder for {@link DeadlineExtensionAttributes}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private final DeadlineExtensionAttributes deadlineExtensionAttributes; private Builder(String courseId, String feedbackSessionName, String userEmail, boolean isInstructor) { @@ -214,7 +214,7 @@ public DeadlineExtensionAttributes build() { /** * Helper class to specify the fields to update in {@link DeadlineExtensionAttributes}. */ - public static class UpdateOptions { + public static final class UpdateOptions { private final String courseId; private final String feedbackSessionName; private final String userEmail; @@ -272,7 +272,7 @@ public String toString() { /** * Builder class to build {@link UpdateOptions}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private Builder(UpdateOptions updateOptions) { super(updateOptions); thisBuilder = this; diff --git a/src/main/java/teammates/common/datatransfer/attributes/EntityAttributes.java b/src/main/java/teammates/common/datatransfer/attributes/EntityAttributes.java index 463b692d0f8b..26395418b255 100644 --- a/src/main/java/teammates/common/datatransfer/attributes/EntityAttributes.java +++ b/src/main/java/teammates/common/datatransfer/attributes/EntityAttributes.java @@ -62,7 +62,7 @@ void addNonEmptyError(String error, List errors) { * * @param type of object being updated */ - protected static class UpdateOption { + protected static final class UpdateOption { private static final UpdateOption EMPTY = new UpdateOption<>(); diff --git a/src/main/java/teammates/common/datatransfer/attributes/FeedbackQuestionAttributes.java b/src/main/java/teammates/common/datatransfer/attributes/FeedbackQuestionAttributes.java index 0872ef36b0d9..ab5ec3869885 100644 --- a/src/main/java/teammates/common/datatransfer/attributes/FeedbackQuestionAttributes.java +++ b/src/main/java/teammates/common/datatransfer/attributes/FeedbackQuestionAttributes.java @@ -512,7 +512,7 @@ public static FeedbackQuestionAttributes.UpdateOptions.Builder updateOptionsBuil /** * A Builder class for {@link FeedbackQuestionAttributes}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private final FeedbackQuestionAttributes feedbackQuestionAttributes; private Builder() { @@ -548,7 +548,7 @@ public FeedbackQuestionAttributes build() { /** * Helper class to specific the fields to update in {@link FeedbackQuestionAttributes}. */ - public static class UpdateOptions { + public static final class UpdateOptions { private String feedbackQuestionId; private UpdateOption questionDetailsOption = UpdateOption.empty(); @@ -590,7 +590,7 @@ public String toString() { /** * Builder class to build {@link UpdateOptions}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private Builder(String feedbackQuestionId) { super(new UpdateOptions(feedbackQuestionId)); diff --git a/src/main/java/teammates/common/datatransfer/attributes/FeedbackResponseAttributes.java b/src/main/java/teammates/common/datatransfer/attributes/FeedbackResponseAttributes.java index cf3bf135cf1a..915bf21378ca 100644 --- a/src/main/java/teammates/common/datatransfer/attributes/FeedbackResponseAttributes.java +++ b/src/main/java/teammates/common/datatransfer/attributes/FeedbackResponseAttributes.java @@ -268,7 +268,7 @@ public static UpdateOptions.Builder updateOptionsBuilder(String feedbackResponse /** * A builder for {@link FeedbackResponseCommentAttributes}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private FeedbackResponseAttributes fra; @@ -307,7 +307,7 @@ public FeedbackResponseAttributes build() { /** * Helper class to specific the fields to update in {@link FeedbackResponseAttributes}. */ - public static class UpdateOptions { + public static final class UpdateOptions { private String feedbackResponseId; private UpdateOption giverOption = UpdateOption.empty(); @@ -341,7 +341,7 @@ public String toString() { /** * Builder class to build {@link UpdateOptions}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private Builder(String feedbackResponseId) { super(new UpdateOptions(feedbackResponseId)); diff --git a/src/main/java/teammates/common/datatransfer/attributes/FeedbackResponseCommentAttributes.java b/src/main/java/teammates/common/datatransfer/attributes/FeedbackResponseCommentAttributes.java index 9369a10117f5..ead8eb168a46 100644 --- a/src/main/java/teammates/common/datatransfer/attributes/FeedbackResponseCommentAttributes.java +++ b/src/main/java/teammates/common/datatransfer/attributes/FeedbackResponseCommentAttributes.java @@ -350,7 +350,7 @@ public static UpdateOptions.Builder updateOptionsBuilder(long feedbackResponseCo /** * A builder for {@link FeedbackResponseCommentAttributes}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private final FeedbackResponseCommentAttributes frca; private Builder() { @@ -416,7 +416,7 @@ public FeedbackResponseCommentAttributes build() { /** * Helper class to specific the fields to update in {@link FeedbackResponseCommentAttributes}. */ - public static class UpdateOptions { + public static final class UpdateOptions { private long feedbackResponseCommentId; private UpdateOption feedbackResponseIdOption = UpdateOption.empty(); @@ -452,7 +452,7 @@ public String toString() { /** * Builder class to build {@link UpdateOptions}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private Builder(Long feedbackResponseCommentId) { super(new UpdateOptions(feedbackResponseCommentId)); diff --git a/src/main/java/teammates/common/datatransfer/attributes/FeedbackSessionAttributes.java b/src/main/java/teammates/common/datatransfer/attributes/FeedbackSessionAttributes.java index 7abefdd69bd9..8b8fdd25dd07 100644 --- a/src/main/java/teammates/common/datatransfer/attributes/FeedbackSessionAttributes.java +++ b/src/main/java/teammates/common/datatransfer/attributes/FeedbackSessionAttributes.java @@ -659,7 +659,7 @@ public static UpdateOptions.Builder updateOptionsBuilder(UpdateOptions updateOpt /** * A builder for {@link FeedbackSessionAttributes}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private final FeedbackSessionAttributes feedbackSessionAttributes; private Builder(String feedbackSessionName, String courseId) { @@ -688,7 +688,7 @@ public FeedbackSessionAttributes build() { /** * Helper class to specific the fields to update in {@link FeedbackSessionAttributes}. */ - public static class UpdateOptions { + public static final class UpdateOptions { private String courseId; private String feedbackSessionName; @@ -752,7 +752,7 @@ public String toString() { /** * Builder class to build {@link UpdateOptions}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private Builder(UpdateOptions updateOptions) { super(updateOptions); diff --git a/src/main/java/teammates/common/datatransfer/attributes/InstructorAttributes.java b/src/main/java/teammates/common/datatransfer/attributes/InstructorAttributes.java index 88d561229f86..5bd9707fd983 100644 --- a/src/main/java/teammates/common/datatransfer/attributes/InstructorAttributes.java +++ b/src/main/java/teammates/common/datatransfer/attributes/InstructorAttributes.java @@ -411,7 +411,7 @@ public static UpdateOptionsWithGoogleId.Builder updateOptionsWithGoogleIdBuilder /** * A builder class for {@link InstructorAttributes}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private final InstructorAttributes instructorAttributes; private Builder(String courseId, String email) { @@ -444,7 +444,7 @@ public InstructorAttributes build() { * *

{@code courseId} and {@code email} is used to identify the instructor. */ - public static class UpdateOptionsWithEmail extends UpdateOptions { + public static final class UpdateOptionsWithEmail extends UpdateOptions { private String courseId; private String email; @@ -476,7 +476,7 @@ public String toString() { /** * Builder class for {@link UpdateOptionsWithEmail}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private UpdateOptionsWithEmail updateOptionsWithEmail; @@ -505,7 +505,7 @@ public UpdateOptionsWithEmail build() { * *

{@code courseId} and {@code googleId} is used to identify the instructor. */ - public static class UpdateOptionsWithGoogleId extends UpdateOptions { + public static final class UpdateOptionsWithGoogleId extends UpdateOptions { private String courseId; private String googleId; @@ -538,7 +538,7 @@ public String toString() { /** * Builder class for {@link UpdateOptionsWithGoogleId}. */ - public static class Builder + public static final class Builder extends BasicBuilder { private UpdateOptionsWithGoogleId updateOptionsWithGoogleId; diff --git a/src/main/java/teammates/common/datatransfer/attributes/NotificationAttributes.java b/src/main/java/teammates/common/datatransfer/attributes/NotificationAttributes.java index 31f353d92557..aeff2647790d 100644 --- a/src/main/java/teammates/common/datatransfer/attributes/NotificationAttributes.java +++ b/src/main/java/teammates/common/datatransfer/attributes/NotificationAttributes.java @@ -260,7 +260,7 @@ public static UpdateOptions.Builder updateOptionsBuilder(UpdateOptions updateOpt /** * A builder for {@link NotificationAttributes}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private final NotificationAttributes notificationAttributes; private Builder(String notificationId) { @@ -281,7 +281,7 @@ public NotificationAttributes build() { /** * Helper class to specific the fields to update in {@link NotificationAttributes}. */ - public static class UpdateOptions { + public static final class UpdateOptions { private String notificationId; private UpdateOption startTimeOption = UpdateOption.empty(); @@ -318,7 +318,7 @@ public String toString() { /** * Builder class to build {@link UpdateOptions}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private Builder(UpdateOptions updateOptions) { super(updateOptions); diff --git a/src/main/java/teammates/common/datatransfer/attributes/StudentAttributes.java b/src/main/java/teammates/common/datatransfer/attributes/StudentAttributes.java index b021983ad3fe..59b3b8939a8d 100644 --- a/src/main/java/teammates/common/datatransfer/attributes/StudentAttributes.java +++ b/src/main/java/teammates/common/datatransfer/attributes/StudentAttributes.java @@ -323,7 +323,7 @@ public static UpdateOptions.Builder updateOptionsBuilder(String courseId, String /** * A builder class for {@link StudentAttributes}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private final StudentAttributes studentAttributes; @@ -345,7 +345,7 @@ public StudentAttributes build() { /** * Helper class to specify the fields to update in {@link StudentAttributes}. */ - public static class UpdateOptions { + public static final class UpdateOptions { private String courseId; private String email; @@ -389,7 +389,7 @@ public String toString() { /** * Builder class to build {@link UpdateOptions}. */ - public static class Builder extends BasicBuilder { + public static final class Builder extends BasicBuilder { private Builder(String courseId, String email) { super(new UpdateOptions(courseId, email)); diff --git a/src/main/java/teammates/common/datatransfer/attributes/UsageStatisticsAttributes.java b/src/main/java/teammates/common/datatransfer/attributes/UsageStatisticsAttributes.java index 47315a9748be..e53ecdccd874 100644 --- a/src/main/java/teammates/common/datatransfer/attributes/UsageStatisticsAttributes.java +++ b/src/main/java/teammates/common/datatransfer/attributes/UsageStatisticsAttributes.java @@ -156,7 +156,7 @@ public static Builder builder(Instant startTime, int timePeriod) { *

Note that we are using a simplified builder pattern here, as opposed to builders of other attributes, * as this entity is designed to be immutable. */ - public static class Builder { + public static final class Builder { private final UsageStatisticsAttributes usa; diff --git a/src/main/java/teammates/common/datatransfer/logs/EmailSentLogDetails.java b/src/main/java/teammates/common/datatransfer/logs/EmailSentLogDetails.java index bebcf3d05148..3a249898d178 100644 --- a/src/main/java/teammates/common/datatransfer/logs/EmailSentLogDetails.java +++ b/src/main/java/teammates/common/datatransfer/logs/EmailSentLogDetails.java @@ -1,6 +1,6 @@ package teammates.common.datatransfer.logs; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.util.EmailType; diff --git a/src/main/java/teammates/common/datatransfer/logs/ExceptionLogDetails.java b/src/main/java/teammates/common/datatransfer/logs/ExceptionLogDetails.java index 2d6da70fc3d1..6c3baea6391a 100644 --- a/src/main/java/teammates/common/datatransfer/logs/ExceptionLogDetails.java +++ b/src/main/java/teammates/common/datatransfer/logs/ExceptionLogDetails.java @@ -2,7 +2,7 @@ import java.util.List; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; /** * Contains specific structure and processing logic for exception log. diff --git a/src/main/java/teammates/common/datatransfer/logs/FeedbackSessionAuditLogDetails.java b/src/main/java/teammates/common/datatransfer/logs/FeedbackSessionAuditLogDetails.java index 0563cada134c..4b5c6c96b7c8 100644 --- a/src/main/java/teammates/common/datatransfer/logs/FeedbackSessionAuditLogDetails.java +++ b/src/main/java/teammates/common/datatransfer/logs/FeedbackSessionAuditLogDetails.java @@ -1,6 +1,6 @@ package teammates.common.datatransfer.logs; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; /** * Contains specific structure and processing logic for feedback session audit log. @@ -10,8 +10,12 @@ public class FeedbackSessionAuditLogDetails extends LogDetails { @Nullable private String courseId; @Nullable + private String feedbackSessionId; + @Nullable private String feedbackSessionName; @Nullable + private String studentId; + @Nullable private String studentEmail; private String accessType; @@ -51,11 +55,29 @@ public void setAccessType(String accessType) { this.accessType = accessType; } + public String getFeedbackSessionId() { + return feedbackSessionId; + } + + public void setFeedbackSessionId(String feedbackSessionId) { + this.feedbackSessionId = feedbackSessionId; + } + + public String getStudentId() { + return studentId; + } + + public void setStudentId(String studentId) { + this.studentId = studentId; + } + @Override public void hideSensitiveInformation() { courseId = null; feedbackSessionName = null; studentEmail = null; + studentId = null; + feedbackSessionId = null; } } diff --git a/src/main/java/teammates/common/datatransfer/logs/GeneralLogEntry.java b/src/main/java/teammates/common/datatransfer/logs/GeneralLogEntry.java index c729551f5c76..2a1adde4c10a 100644 --- a/src/main/java/teammates/common/datatransfer/logs/GeneralLogEntry.java +++ b/src/main/java/teammates/common/datatransfer/logs/GeneralLogEntry.java @@ -2,7 +2,7 @@ import java.util.Map; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; /** * Represents a log entry and contains the fields that are more important diff --git a/src/main/java/teammates/common/datatransfer/logs/LogDetails.java b/src/main/java/teammates/common/datatransfer/logs/LogDetails.java index 0c8582a0f4fa..5ad15b43ce37 100644 --- a/src/main/java/teammates/common/datatransfer/logs/LogDetails.java +++ b/src/main/java/teammates/common/datatransfer/logs/LogDetails.java @@ -1,6 +1,6 @@ package teammates.common.datatransfer.logs; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; /** * Holds the details for a specific log event. diff --git a/src/main/java/teammates/common/datatransfer/logs/QueryLogsParams.java b/src/main/java/teammates/common/datatransfer/logs/QueryLogsParams.java index 01f93a78c294..c1a5f1e30f9f 100644 --- a/src/main/java/teammates/common/datatransfer/logs/QueryLogsParams.java +++ b/src/main/java/teammates/common/datatransfer/logs/QueryLogsParams.java @@ -100,7 +100,7 @@ public Integer getPageSize() { /** * Builder for {@link QueryLogsParams}. */ - public static class Builder { + public static final class Builder { private QueryLogsParams queryLogsParams; private Builder(long startTime, long endTime) { diff --git a/src/main/java/teammates/common/datatransfer/logs/RequestLogDetails.java b/src/main/java/teammates/common/datatransfer/logs/RequestLogDetails.java index 6aa1e51131ab..c4eb4f79f436 100644 --- a/src/main/java/teammates/common/datatransfer/logs/RequestLogDetails.java +++ b/src/main/java/teammates/common/datatransfer/logs/RequestLogDetails.java @@ -2,7 +2,7 @@ import java.util.Map; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; /** * Contains specific structure and processing logic for HTTP request log. diff --git a/src/main/java/teammates/common/datatransfer/questions/FeedbackConstantSumQuestionDetails.java b/src/main/java/teammates/common/datatransfer/questions/FeedbackConstantSumQuestionDetails.java index ac1000fc9a34..b09892849055 100644 --- a/src/main/java/teammates/common/datatransfer/questions/FeedbackConstantSumQuestionDetails.java +++ b/src/main/java/teammates/common/datatransfer/questions/FeedbackConstantSumQuestionDetails.java @@ -6,7 +6,7 @@ import java.util.Objects; import java.util.Set; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.attributes.FeedbackQuestionAttributes; import teammates.common.util.FieldValidator; @@ -136,7 +136,7 @@ public List validateQuestionDetails() { // the validations below are only for constSumOptions int totalPoints = pointsPerOption ? points * constSumOptions.size() : points; - double evenPointDistribution = ((double) totalPoints) / ((double) constSumOptions.size()); + double evenPointDistribution = 1.0d * totalPoints / constSumOptions.size(); if (minPoint != null) { commonBoundaryValidation(errors, minPoint, totalPoints, MIN_POINT_STRING); diff --git a/src/main/java/teammates/common/datatransfer/questions/FeedbackTextQuestionDetails.java b/src/main/java/teammates/common/datatransfer/questions/FeedbackTextQuestionDetails.java index 23e9bbf44496..aad5118988f7 100644 --- a/src/main/java/teammates/common/datatransfer/questions/FeedbackTextQuestionDetails.java +++ b/src/main/java/teammates/common/datatransfer/questions/FeedbackTextQuestionDetails.java @@ -3,7 +3,7 @@ import java.util.ArrayList; import java.util.List; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.attributes.FeedbackQuestionAttributes; import teammates.storage.sqlentity.FeedbackQuestion; diff --git a/src/main/java/teammates/common/util/Const.java b/src/main/java/teammates/common/util/Const.java index b24d0ded6488..dc9659f867b9 100644 --- a/src/main/java/teammates/common/util/Const.java +++ b/src/main/java/teammates/common/util/Const.java @@ -45,6 +45,11 @@ public final class Const { public static final String MISSING_RESPONSE_TEXT = "No Response"; + public static final Duration STUDENT_ACTIVITY_LOGS_UPDATE_INTERVAL = Duration.ofMinutes(15); + public static final Duration STUDENT_ACTIVITY_LOGS_FILTER_WINDOW = Duration.ofSeconds(2); + + public static final String ACCOUNT_REQUEST_NOT_FOUND = "Account request with id = %s not found"; + // These constants are used as variable values to mean that the variable is in a 'special' state. public static final int INT_UNINITIALIZED = -9999; @@ -123,6 +128,11 @@ public static class ParamsNames { public static final String IS_CREATING_ACCOUNT = "iscreatingaccount"; public static final String IS_INSTRUCTOR = "isinstructor"; + public static final String FEEDBACK_SESSION_ID = "fsid"; + + public static final String ACCOUNT_REQUEST_ID = "id"; + public static final String ACCOUNT_REQUEST_STATUS = "status"; + public static final String FEEDBACK_SESSION_NAME = "fsname"; public static final String FEEDBACK_SESSION_STARTTIME = "starttime"; public static final String FEEDBACK_SESSION_ENDTIME = "endtime"; @@ -144,6 +154,7 @@ public static class ParamsNames { public static final String PREVIEWAS = "previewas"; + public static final String STUDENT_SQL_ID = "studentid"; public static final String STUDENT_ID = "googleid"; public static final String INVITER_ID = "invitergoogleid"; @@ -313,6 +324,8 @@ public static class WebPageURIs { public static final String SESSION_RESULTS_PAGE = URI_PREFIX + "/sessions/result"; public static final String SESSION_SUBMISSION_PAGE = URI_PREFIX + "/sessions/submission"; public static final String SESSIONS_LINK_RECOVERY_PAGE = FRONT_PAGE + "/help/session-links-recovery"; + + public static final String ACCOUNT_REQUEST_PAGE = FRONT_PAGE + "/request"; } /** @@ -332,7 +345,9 @@ public static class ResourceURIs { public static final String ACCOUNT = URI_PREFIX + "/account"; public static final String ACCOUNT_RESET = URI_PREFIX + "/account/reset"; public static final String ACCOUNT_REQUEST = URI_PREFIX + "/account/request"; + public static final String ACCOUNT_REQUESTS = URI_PREFIX + "/account/requests"; public static final String ACCOUNT_REQUEST_RESET = ACCOUNT_REQUEST + "/reset"; + public static final String ACCOUNT_REQUEST_REJECTION = ACCOUNT_REQUEST + "/rejection"; public static final String ACCOUNTS = URI_PREFIX + "/accounts"; public static final String RESPONSE_COMMENT = URI_PREFIX + "/responsecomment"; public static final String COURSE = URI_PREFIX + "/course"; @@ -401,6 +416,8 @@ public static class CronJobURIs { URI_PREFIX + "/feedbackSessionPublishedReminders"; public static final String AUTOMATED_USAGE_STATISTICS_COLLECTION = URI_PREFIX + "/calculateUsageStatistics"; + public static final String AUTOMATED_FEEDBACK_SESSION_LOGS_PROCESSING = + URI_PREFIX + "/updateFeedbackSessionLogs"; } /** diff --git a/src/main/java/teammates/common/util/EmailType.java b/src/main/java/teammates/common/util/EmailType.java index af649debb7b3..a42280ba7f08 100644 --- a/src/main/java/teammates/common/util/EmailType.java +++ b/src/main/java/teammates/common/util/EmailType.java @@ -23,6 +23,9 @@ public enum EmailType { NEW_INSTRUCTOR_ACCOUNT("TEAMMATES: Welcome to TEAMMATES! %s"), STUDENT_COURSE_JOIN("TEAMMATES: Invitation to join course [%s][Course ID: %s]"), STUDENT_COURSE_REJOIN_AFTER_GOOGLE_ID_RESET("TEAMMATES: Your account has been reset for course [%s][Course ID: %s]"), + NEW_ACCOUNT_REQUEST_ADMIN_ALERT("TEAMMATES (Action Needed): New Account Request Received"), + NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT("TEAMMATES: Acknowledgement of Instructor Account Request"), + ACCOUNT_REQUEST_REJECTION("TEAMMATES: %s"), INSTRUCTOR_COURSE_JOIN("TEAMMATES: Invitation to join course as an instructor [%s][Course ID: %s]"), INSTRUCTOR_COURSE_REJOIN_AFTER_GOOGLE_ID_RESET("TEAMMATES: Your account has been reset for course [%s][Course ID: %s]"), USER_COURSE_REGISTER("TEAMMATES: Registered for Course [%s][Course ID: %s]"), diff --git a/src/main/java/teammates/common/util/FieldValidator.java b/src/main/java/teammates/common/util/FieldValidator.java index 10b1b34a6824..1e46adbff340 100644 --- a/src/main/java/teammates/common/util/FieldValidator.java +++ b/src/main/java/teammates/common/util/FieldValidator.java @@ -46,7 +46,7 @@ public final class FieldValidator { public static final int SECTION_NAME_MAX_LENGTH = 60; public static final String INSTITUTE_NAME_FIELD_NAME = "institute name"; - public static final int INSTITUTE_NAME_MAX_LENGTH = 64; + public static final int INSTITUTE_NAME_MAX_LENGTH = 128; // email-related public static final String EMAIL_FIELD_NAME = "email"; diff --git a/src/main/java/teammates/common/util/HibernateUtil.java b/src/main/java/teammates/common/util/HibernateUtil.java index e35238018e5a..56323acc582c 100644 --- a/src/main/java/teammates/common/util/HibernateUtil.java +++ b/src/main/java/teammates/common/util/HibernateUtil.java @@ -2,6 +2,11 @@ import java.util.List; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.CriteriaQuery; + import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; @@ -19,6 +24,7 @@ import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.FeedbackResponseComment; import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; import teammates.storage.sqlentity.Instructor; import teammates.storage.sqlentity.Notification; import teammates.storage.sqlentity.ReadNotification; @@ -46,11 +52,6 @@ import teammates.storage.sqlentity.responses.FeedbackRubricResponse; import teammates.storage.sqlentity.responses.FeedbackTextResponse; -import jakarta.persistence.TypedQuery; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaDelete; -import jakarta.persistence.criteria.CriteriaQuery; - /** * Utility class for Hibernate related methods. */ @@ -91,7 +92,8 @@ public final class HibernateUtil { FeedbackRankRecipientsResponse.class, FeedbackRubricResponse.class, FeedbackTextResponse.class, - FeedbackResponseComment.class); + FeedbackResponseComment.class, + FeedbackSessionLog.class); private HibernateUtil() { // Utility class @@ -116,7 +118,7 @@ public static void buildSessionFactory(String dbUrl, String username, String pas .setProperty("hibernate.connection.username", username) .setProperty("hibernate.connection.password", password) .setProperty("hibernate.connection.url", dbUrl) - .setProperty("hibernate.hbm2ddl.auto", "update") + .setProperty("hibernate.hbm2ddl.auto", "validate") .setProperty("show_sql", "true") .setProperty("hibernate.current_session_context_class", "thread") .setProperty("hibernate.hikari.minimumIdle", "10") @@ -130,6 +132,10 @@ public static void buildSessionFactory(String dbUrl, String username, String pas .setProperty("hibernate.jdbc.fetch_size", "50") .addPackage("teammates.storage.sqlentity"); + if (Config.IS_DEV_SERVER) { + config.setProperty("hibernate.hbm2ddl.auto", "update"); + } + for (Class cls : ANNOTATED_CLASSES) { config = config.addAnnotatedClass(cls); } @@ -152,7 +158,7 @@ private static SessionFactory getSessionFactory() { * @see SessionFactory#getCurrentSession() */ private static Session getCurrentSession() { - return HibernateUtil.getSessionFactory().getCurrentSession(); + return getSessionFactory().getCurrentSession(); } /** @@ -188,7 +194,7 @@ public static void setSessionFactory(SessionFactory sessionFactory) { * @see Transaction#begin() */ public static void beginTransaction() { - Transaction transaction = HibernateUtil.getCurrentSession().getTransaction(); + Transaction transaction = getCurrentSession().getTransaction(); transaction.begin(); } @@ -197,7 +203,7 @@ public static void beginTransaction() { * @see Transaction#rollback() */ public static void rollbackTransaction() { - Session session = HibernateUtil.getCurrentSession(); + Session session = getCurrentSession(); if (session.getTransaction().getStatus() == TransactionStatus.ACTIVE || session.getTransaction().getStatus() == TransactionStatus.MARKED_ROLLBACK) { session.getTransaction().rollback(); @@ -206,10 +212,10 @@ public static void rollbackTransaction() { /** * Commit the current resource transaction, writing any unflushed changes to the database. - * @see Session#commit() + * @see Transaction#commit() */ public static void commitTransaction() { - Transaction transaction = HibernateUtil.getCurrentSession().getTransaction(); + Transaction transaction = getCurrentSession().getTransaction(); transaction.commit(); } @@ -218,7 +224,7 @@ public static void commitTransaction() { * @see Session#flush() */ public static void flushSession() { - HibernateUtil.getCurrentSession().flush(); + getCurrentSession().flush(); } /** @@ -226,7 +232,7 @@ public static void flushSession() { * @see Session#clear() */ public static void clearSession() { - HibernateUtil.getCurrentSession().clear(); + getCurrentSession().clear(); } /** @@ -235,7 +241,7 @@ public static void clearSession() { * @see Session#get(Class, Object) */ public static T get(Class entityType, Object id) { - return HibernateUtil.getCurrentSession().get(entityType, id); + return getCurrentSession().get(entityType, id); } /** @@ -255,7 +261,7 @@ public static T getReference(Class entityType, Object * @see Session#get(Class, Object) */ public static T getBySimpleNaturalId(Class entityType, Object id) { - return HibernateUtil.getCurrentSession().bySimpleNaturalId(entityType).load(id); + return getCurrentSession().bySimpleNaturalId(entityType).load(id); } /** @@ -263,7 +269,7 @@ public static T getBySimpleNaturalId(Class entityType, * @see Session#merge(E) */ public static E merge(E object) { - return HibernateUtil.getCurrentSession().merge(object); + return getCurrentSession().merge(object); } /** @@ -271,7 +277,7 @@ public static E merge(E object) { * @see Session#persist(Object) */ public static void persist(BaseEntity entity) { - HibernateUtil.getCurrentSession().persist(entity); + getCurrentSession().persist(entity); } /** @@ -279,14 +285,24 @@ public static void persist(BaseEntity entity) { * @see Session#remove(Object) */ public static void remove(BaseEntity entity) { - HibernateUtil.getCurrentSession().remove(entity); + getCurrentSession().remove(entity); } /** * Create and execute a {@code MutationQuery} for the given delete criteria tree. */ public static void executeDelete(CriteriaDelete cd) { - HibernateUtil.getCurrentSession().createMutationQuery(cd).executeUpdate(); + getCurrentSession().createMutationQuery(cd).executeUpdate(); + } + + /** + * Return a reference to the persistent instance with the given class and + * identifier,making the assumption that the instance is still persistent in the + * database. + * @see Session#getReference(Class, Object) + */ + public static T getReference(Class entityType, Object id) { + return getCurrentSession().getReference(entityType, id); } } diff --git a/src/main/java/teammates/common/util/HttpRequestHelper.java b/src/main/java/teammates/common/util/HttpRequestHelper.java index a51686539c0d..a8b4a0187561 100644 --- a/src/main/java/teammates/common/util/HttpRequestHelper.java +++ b/src/main/java/teammates/common/util/HttpRequestHelper.java @@ -9,8 +9,8 @@ import java.util.Map; import java.util.stream.Collectors; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; /** * Holds {@link HttpServletRequest}-related helper functions. diff --git a/src/main/java/teammates/common/util/JsonUtils.java b/src/main/java/teammates/common/util/JsonUtils.java index 7f121801e05c..03505a59307a 100644 --- a/src/main/java/teammates/common/util/JsonUtils.java +++ b/src/main/java/teammates/common/util/JsonUtils.java @@ -6,6 +6,8 @@ import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import jakarta.persistence.OneToMany; + import com.google.gson.ExclusionStrategy; import com.google.gson.FieldAttributes; import com.google.gson.Gson; @@ -48,8 +50,6 @@ import teammates.storage.sqlentity.responses.FeedbackRubricResponse; import teammates.storage.sqlentity.responses.FeedbackTextResponse; -import jakarta.persistence.OneToMany; - /** * Provides means to handle, manipulate, and convert JSON objects to/from strings. */ @@ -153,7 +153,7 @@ public static JsonElement parse(String json) { return JsonParser.parseString(json); } - private static class HibernateExclusionStrategy implements ExclusionStrategy { + private static final class HibernateExclusionStrategy implements ExclusionStrategy { @Override public boolean shouldSkipField(FieldAttributes f) { @@ -167,7 +167,7 @@ public boolean shouldSkipClass(Class clazz) { } } - private static class UserAdapter implements JsonSerializer, JsonDeserializer { + private static final class UserAdapter implements JsonSerializer, JsonDeserializer { @Override public JsonElement serialize(User user, Type type, JsonSerializationContext context) { @@ -196,7 +196,7 @@ public User deserialize(JsonElement element, Type type, JsonDeserializationConte } } - private static class InstantAdapter implements JsonSerializer, JsonDeserializer { + private static final class InstantAdapter implements JsonSerializer, JsonDeserializer { @Override public JsonElement serialize(Instant instant, Type type, JsonSerializationContext context) { @@ -213,7 +213,7 @@ public Instant deserialize(JsonElement element, Type type, JsonDeserializationCo } } - private static class ZoneIdAdapter implements JsonSerializer, JsonDeserializer { + private static final class ZoneIdAdapter implements JsonSerializer, JsonDeserializer { @Override public JsonElement serialize(ZoneId zoneId, Type type, JsonSerializationContext context) { @@ -230,7 +230,7 @@ public ZoneId deserialize(JsonElement element, Type type, JsonDeserializationCon } } - private static class DurationMinutesAdapter implements JsonSerializer, JsonDeserializer { + private static final class DurationMinutesAdapter implements JsonSerializer, JsonDeserializer { @Override public JsonElement serialize(Duration duration, Type type, JsonSerializationContext context) { @@ -247,7 +247,7 @@ public Duration deserialize(JsonElement element, Type type, JsonDeserializationC } } - private static class FeedbackResponseAdapter implements JsonSerializer, + private static final class FeedbackResponseAdapter implements JsonSerializer, JsonDeserializer { @Override @@ -306,7 +306,7 @@ public FeedbackResponse deserialize(JsonElement json, Type typeOfT, JsonDeserial } } - private static class FeedbackResponseDetailsAdapter implements JsonSerializer, + private static final class FeedbackResponseDetailsAdapter implements JsonSerializer, JsonDeserializer { @Override @@ -323,7 +323,7 @@ public FeedbackResponseDetails deserialize(JsonElement json, Type typeOfT, JsonD } - private static class FeedbackQuestionAdapter implements JsonSerializer, + private static final class FeedbackQuestionAdapter implements JsonSerializer, JsonDeserializer { @Override @@ -382,7 +382,7 @@ public FeedbackQuestion deserialize(JsonElement json, Type typeOfT, JsonDeserial } } - private static class FeedbackQuestionDetailsAdapter implements JsonSerializer, + private static final class FeedbackQuestionDetailsAdapter implements JsonSerializer, JsonDeserializer { @Override @@ -398,7 +398,7 @@ public FeedbackQuestionDetails deserialize(JsonElement json, Type typeOfT, JsonD } } - private static class LogDetailsAdapter implements JsonSerializer, JsonDeserializer { + private static final class LogDetailsAdapter implements JsonSerializer, JsonDeserializer { @Override public JsonElement serialize(LogDetails src, Type typeOfSrc, JsonSerializationContext context) { diff --git a/src/main/java/teammates/common/util/Logger.java b/src/main/java/teammates/common/util/Logger.java index bf3bb487da3f..c48061cd6702 100644 --- a/src/main/java/teammates/common/util/Logger.java +++ b/src/main/java/teammates/common/util/Logger.java @@ -8,7 +8,7 @@ import java.util.List; import java.util.Map; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import com.google.common.reflect.TypeToken; diff --git a/src/main/java/teammates/common/util/RequestTracer.java b/src/main/java/teammates/common/util/RequestTracer.java index d908f5c378ec..2ad63f37f785 100644 --- a/src/main/java/teammates/common/util/RequestTracer.java +++ b/src/main/java/teammates/common/util/RequestTracer.java @@ -78,7 +78,7 @@ public static void init(String traceId, String spanId, int timeoutInSeconds) { THREAD_LOCAL.set(new RequestTrace(traceId, spanId, timeoutInSeconds)); } - private static class RequestTrace { + private static final class RequestTrace { private final String traceId; private final String spanId; private final long initTimestamp; diff --git a/src/main/java/teammates/common/util/Templates.java b/src/main/java/teammates/common/util/Templates.java index 524c84bc2c61..1fea9a614050 100644 --- a/src/main/java/teammates/common/util/Templates.java +++ b/src/main/java/teammates/common/util/Templates.java @@ -32,6 +32,10 @@ public static String populateTemplate(String template, String... keyValuePairs) * Collection of templates of emails to be sent by the system. */ public static class EmailTemplates { + public static final String ADMIN_NEW_ACCOUNT_REQUEST_ALERT = + FileHelper.readResourceFile("adminEmailTemplate-newAccountRequestAlert.html"); + public static final String INSTRUCTOR_NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT = + FileHelper.readResourceFile("instructorEmailTemplate-newAccountRequestAcknowledgement.html"); public static final String USER_COURSE_JOIN = FileHelper.readResourceFile("userEmailTemplate-courseJoin.html"); public static final String USER_COURSE_REGISTER = diff --git a/src/main/java/teammates/common/util/TimeHelper.java b/src/main/java/teammates/common/util/TimeHelper.java index 8122ff6f39a2..de7d58f796aa 100644 --- a/src/main/java/teammates/common/util/TimeHelper.java +++ b/src/main/java/teammates/common/util/TimeHelper.java @@ -29,6 +29,18 @@ public static Instant getInstantNearestHourBefore(Instant instant) { return parseInstant(nearestHourString); } + /** + * Returns an Instant that represents the nearest quarter hour before the given object. + * + *

The time zone used is assumed to be the default timezone, namely UTC. + */ + public static Instant getInstantNearestQuarterHourBefore(Instant instant) { + ZonedDateTime zdt = instant.atZone(ZoneId.of(Const.DEFAULT_TIME_ZONE)); + int minutesPastQuarter = zdt.getMinute() % 15; + ZonedDateTime nearestQuarterZdt = zdt.minusMinutes(minutesPastQuarter).withSecond(0).withNano(0); + return nearestQuarterZdt.toInstant(); + } + /** * Returns an Instant that is offset by a number of days from now. * diff --git a/src/main/java/teammates/logic/api/Logic.java b/src/main/java/teammates/logic/api/Logic.java index 12bf9b5e8ddb..4c0835e626c3 100644 --- a/src/main/java/teammates/logic/api/Logic.java +++ b/src/main/java/teammates/logic/api/Logic.java @@ -6,7 +6,7 @@ import java.util.Map; import java.util.Set; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.DataBundle; import teammates.common.datatransfer.FeedbackQuestionRecipient; diff --git a/src/main/java/teammates/logic/api/LogsProcessor.java b/src/main/java/teammates/logic/api/LogsProcessor.java index eac5f73f755a..57d889be4881 100644 --- a/src/main/java/teammates/logic/api/LogsProcessor.java +++ b/src/main/java/teammates/logic/api/LogsProcessor.java @@ -2,6 +2,7 @@ import java.time.Instant; import java.util.List; +import java.util.UUID; import teammates.common.datatransfer.FeedbackSessionLogEntry; import teammates.common.datatransfer.QueryLogsResults; @@ -51,12 +52,19 @@ public void createFeedbackSessionLog(String courseId, String email, String fsNam } /** - * Gets the feedback session logs as filtered by the given parameters. + * Creates a feedback session log. + */ + public void createFeedbackSessionLog(String courseId, UUID studentId, UUID fsId, String fslType) { + service.createFeedbackSessionLog(courseId, studentId, fsId, fslType); + } + + /** + * Gets the feedback session logs as filtered by the given parameters ordered by ascending timestamp. * @param email Can be null */ - public List getFeedbackSessionLogs(String courseId, String email, + public List getOrderedFeedbackSessionLogs(String courseId, String email, long startTime, long endTime, String fsName) { - return service.getFeedbackSessionLogs(courseId, email, startTime, endTime, fsName); + return service.getOrderedFeedbackSessionLogs(courseId, email, startTime, endTime, fsName); } /** diff --git a/src/main/java/teammates/logic/api/TaskQueuer.java b/src/main/java/teammates/logic/api/TaskQueuer.java index a3db7d7d3591..6d7e9acdbebb 100644 --- a/src/main/java/teammates/logic/api/TaskQueuer.java +++ b/src/main/java/teammates/logic/api/TaskQueuer.java @@ -194,9 +194,9 @@ public void scheduleEmailsForSending(List emails) { int oneHourInMillis = 60 * 60 * 1000; int emailIntervalMillis = Math.min(5000, oneHourInMillis / emails.size()); - int numberOfEmailsSent = 0; + long numberOfEmailsSent = 0L; for (EmailWrapper email : emails) { - long emailDelayTimer = (long) numberOfEmailsSent * (long) emailIntervalMillis; + long emailDelayTimer = numberOfEmailsSent * emailIntervalMillis; scheduleEmailForSending(email, emailDelayTimer); numberOfEmailsSent++; } @@ -218,15 +218,13 @@ public void scheduleInstructorForSearchIndexing(String courseId, String email) { } /** - * Schedules for the search indexing of the account request identified by {@code email} and {@code institute}. + * Schedules for the search indexing of the account request identified by {@code id}. * - * @param email the email associated with the account request - * @param institute the institute associated with the account request + * @param id the id associated with the account request */ - public void scheduleAccountRequestForSearchIndexing(String email, String institute) { + public void scheduleAccountRequestForSearchIndexing(String id) { Map paramMap = new HashMap<>(); - paramMap.put(ParamsNames.INSTRUCTOR_EMAIL, email); - paramMap.put(ParamsNames.INSTRUCTOR_INSTITUTION, institute); + paramMap.put(ParamsNames.ACCOUNT_REQUEST_ID, id); addTask(TaskQueue.SEARCH_INDEXING_QUEUE_NAME, TaskQueue.ACCOUNT_REQUEST_SEARCH_INDEXING_WORKER_URL, paramMap, null); diff --git a/src/main/java/teammates/logic/core/FeedbackQuestionsLogic.java b/src/main/java/teammates/logic/core/FeedbackQuestionsLogic.java index 59633bbfff39..4e751fd5f251 100644 --- a/src/main/java/teammates/logic/core/FeedbackQuestionsLogic.java +++ b/src/main/java/teammates/logic/core/FeedbackQuestionsLogic.java @@ -9,7 +9,7 @@ import java.util.Set; import java.util.stream.Collectors; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.AttributesDeletionQuery; import teammates.common.datatransfer.CourseRoster; diff --git a/src/main/java/teammates/logic/core/FeedbackResponseCommentsLogic.java b/src/main/java/teammates/logic/core/FeedbackResponseCommentsLogic.java index 92c76ec58cbb..857d92daf592 100644 --- a/src/main/java/teammates/logic/core/FeedbackResponseCommentsLogic.java +++ b/src/main/java/teammates/logic/core/FeedbackResponseCommentsLogic.java @@ -4,7 +4,7 @@ import java.util.List; import java.util.Set; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.AttributesDeletionQuery; import teammates.common.datatransfer.CourseRoster; diff --git a/src/main/java/teammates/logic/core/FeedbackResponsesLogic.java b/src/main/java/teammates/logic/core/FeedbackResponsesLogic.java index c16af38d37b6..74a5a99dcdf1 100644 --- a/src/main/java/teammates/logic/core/FeedbackResponsesLogic.java +++ b/src/main/java/teammates/logic/core/FeedbackResponsesLogic.java @@ -10,7 +10,7 @@ import java.util.Map; import java.util.Set; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.AttributesDeletionQuery; import teammates.common.datatransfer.CourseRoster; @@ -1158,7 +1158,7 @@ boolean canInstructorsSeeComment(FeedbackResponseCommentAttributes feedbackRespo /** * Set contains only unique response. */ - private static class UniqueResponsesSet { + private static final class UniqueResponsesSet { private final Set responseIds; private final List responses; diff --git a/src/main/java/teammates/logic/core/LogicStarter.java b/src/main/java/teammates/logic/core/LogicStarter.java index e82ba1d8c4d7..a3db4ad2ff3f 100644 --- a/src/main/java/teammates/logic/core/LogicStarter.java +++ b/src/main/java/teammates/logic/core/LogicStarter.java @@ -1,7 +1,7 @@ package teammates.logic.core; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; import teammates.common.util.Logger; diff --git a/src/main/java/teammates/logic/external/GoogleCloudLoggingService.java b/src/main/java/teammates/logic/external/GoogleCloudLoggingService.java index 3c2b7000ce7a..3da2dc68ef84 100644 --- a/src/main/java/teammates/logic/external/GoogleCloudLoggingService.java +++ b/src/main/java/teammates/logic/external/GoogleCloudLoggingService.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; import com.google.api.gax.paging.Page; @@ -115,7 +116,14 @@ public void createFeedbackSessionLog(String courseId, String email, String fsNam } @Override - public List getFeedbackSessionLogs(String courseId, String email, + public void createFeedbackSessionLog(String courseId, UUID studentId, UUID fsId, String fslType) { + // This method is not necessary for production usage because a feedback session log + // is already separately created through the standardized logging infrastructure. + // However, this method is not removed as it is necessary to assist in local testing. + } + + @Override + public List getOrderedFeedbackSessionLogs(String courseId, String email, long startTime, long endTime, String fsName) { List filters = new ArrayList<>(); if (courseId != null) { @@ -131,6 +139,7 @@ public List getFeedbackSessionLogs(String courseId, Str .withLogEvent(LogEvent.FEEDBACK_SESSION_AUDIT.name()) .withSeverityLevel(LogSeverity.INFO) .withExtraFilters(String.join("\n", filters)) + .withOrder(ASCENDING_ORDER) .build(); LogSearchParams logSearchParams = LogSearchParams.from(queryLogsParams) .addLogName(STDOUT_LOG_NAME) @@ -153,8 +162,16 @@ public List getFeedbackSessionLogs(String courseId, Str continue; } - FeedbackSessionLogEntry fslEntry = new FeedbackSessionLogEntry(details.getStudentEmail(), - details.getFeedbackSessionName(), details.getAccessType(), timestamp); + UUID studentId = details.getStudentId() != null ? UUID.fromString(details.getStudentId()) : null; + UUID fsId = details.getFeedbackSessionId() != null ? UUID.fromString(details.getFeedbackSessionId()) : null; + FeedbackSessionLogEntry fslEntry; + if (fsId != null && studentId != null) { + fslEntry = new FeedbackSessionLogEntry(details.getCourseId(), studentId, fsId, details.getAccessType(), + timestamp); + } else { + fslEntry = new FeedbackSessionLogEntry(details.getCourseId(), details.getStudentEmail(), + details.getFeedbackSessionName(), details.getAccessType(), timestamp); + } fsLogEntries.add(fslEntry); } @@ -281,7 +298,7 @@ private List convertLogSearchParams(LogSearchParams s, int page /** * Contains params to be used for the searching of logs. */ - private static class LogSearchParams { + private static final class LogSearchParams { private final List logName = new ArrayList<>(); private String resourceType; private QueryLogsParams queryLogsParams; diff --git a/src/main/java/teammates/logic/external/LocalLoggingService.java b/src/main/java/teammates/logic/external/LocalLoggingService.java index 03c90f52b4a5..9750206fa379 100644 --- a/src/main/java/teammates/logic/external/LocalLoggingService.java +++ b/src/main/java/teammates/logic/external/LocalLoggingService.java @@ -6,6 +6,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -203,13 +204,20 @@ private boolean isRequestFilterSatisfied(LogDetails details, String actionClassF @Override public void createFeedbackSessionLog(String courseId, String email, String fsName, String fslType) { - FeedbackSessionLogEntry logEntry = new FeedbackSessionLogEntry(email, fsName, + FeedbackSessionLogEntry logEntry = new FeedbackSessionLogEntry(courseId, email, + fsName, fslType, Instant.now().toEpochMilli()); + FEEDBACK_SESSION_LOG_ENTRIES.computeIfAbsent(courseId, k -> new ArrayList<>()).add(logEntry); + } + + @Override + public void createFeedbackSessionLog(String courseId, UUID studentId, UUID fsId, String fslType) { + FeedbackSessionLogEntry logEntry = new FeedbackSessionLogEntry(courseId, studentId, fsId, fslType, Instant.now().toEpochMilli()); FEEDBACK_SESSION_LOG_ENTRIES.computeIfAbsent(courseId, k -> new ArrayList<>()).add(logEntry); } @Override - public List getFeedbackSessionLogs(String courseId, String email, + public List getOrderedFeedbackSessionLogs(String courseId, String email, long startTime, long endTime, String fsName) { return FEEDBACK_SESSION_LOG_ENTRIES .getOrDefault(courseId, new ArrayList<>()) @@ -218,6 +226,7 @@ public List getFeedbackSessionLogs(String courseId, Str .filter(log -> fsName == null || log.getFeedbackSessionName().equals(fsName)) .filter(log -> log.getTimestamp() >= startTime) .filter(log -> log.getTimestamp() <= endTime) + .sorted() .collect(Collectors.toList()); } diff --git a/src/main/java/teammates/logic/external/LogService.java b/src/main/java/teammates/logic/external/LogService.java index 5a85c59fafb7..08f3653d2d32 100644 --- a/src/main/java/teammates/logic/external/LogService.java +++ b/src/main/java/teammates/logic/external/LogService.java @@ -1,6 +1,7 @@ package teammates.logic.external; import java.util.List; +import java.util.UUID; import teammates.common.datatransfer.FeedbackSessionLogEntry; import teammates.common.datatransfer.QueryLogsResults; @@ -22,8 +23,13 @@ public interface LogService { void createFeedbackSessionLog(String courseId, String email, String fsName, String fslType); /** - * Gets the feedback session logs as filtered by the given parameters. + * Creates a feedback session log for migrated courses. */ - List getFeedbackSessionLogs(String courseId, String email, + void createFeedbackSessionLog(String courseId, UUID studentId, UUID fsId, String fslType); + + /** + * Gets the feedback session logs as filtered by the given parameters ordered by ascending timestamp. + */ + List getOrderedFeedbackSessionLogs(String courseId, String email, long startTime, long endTime, String fsName); } diff --git a/src/main/java/teammates/logic/external/MailgunService.java b/src/main/java/teammates/logic/external/MailgunService.java index 1fd74de88a77..4256b64975d7 100644 --- a/src/main/java/teammates/logic/external/MailgunService.java +++ b/src/main/java/teammates/logic/external/MailgunService.java @@ -20,7 +20,7 @@ /** * Email sender service provided by Mailgun. * - * @see https://cloud.google.com/appengine/docs/standard/java11/sending-messages#mailgun + * @see https://cloud.google.com/appengine/docs/standard/sending-messages#mailgun * @see FormDataMultiPart */ public class MailgunService implements EmailSenderService { diff --git a/src/main/java/teammates/logic/external/MailjetService.java b/src/main/java/teammates/logic/external/MailjetService.java index e319690d26a6..d54e5871e55b 100644 --- a/src/main/java/teammates/logic/external/MailjetService.java +++ b/src/main/java/teammates/logic/external/MailjetService.java @@ -20,7 +20,7 @@ /** * Email sender service provided by Mailjet. * - * @see https://cloud.google.com/appengine/docs/standard/java11/sending-messages#mailjet + * @see https://cloud.google.com/appengine/docs/standard/sending-messages#mailjet * @see MailjetClient * @see MailjetRequest * @see MailjetResponse diff --git a/src/main/java/teammates/logic/external/SendgridService.java b/src/main/java/teammates/logic/external/SendgridService.java index df5ce52860e3..7526ca946e11 100644 --- a/src/main/java/teammates/logic/external/SendgridService.java +++ b/src/main/java/teammates/logic/external/SendgridService.java @@ -22,7 +22,7 @@ /** * Email sender service provided by SendGrid. * - * @see https://cloud.google.com/appengine/docs/standard/java11/sending-messages#sendgrid + * @see https://cloud.google.com/appengine/docs/standard/sending-messages#sendgrid * @see SendGrid */ public class SendgridService implements EmailSenderService { diff --git a/src/main/java/teammates/sqllogic/api/Logic.java b/src/main/java/teammates/sqllogic/api/Logic.java index f83ff914ec7e..659f27750c50 100644 --- a/src/main/java/teammates/sqllogic/api/Logic.java +++ b/src/main/java/teammates/sqllogic/api/Logic.java @@ -6,8 +6,9 @@ import java.util.Set; import java.util.UUID; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.datatransfer.FeedbackQuestionRecipient; import teammates.common.datatransfer.FeedbackResultFetchType; import teammates.common.datatransfer.NotificationStyle; @@ -29,6 +30,7 @@ import teammates.sqllogic.core.FeedbackQuestionsLogic; import teammates.sqllogic.core.FeedbackResponseCommentsLogic; import teammates.sqllogic.core.FeedbackResponsesLogic; +import teammates.sqllogic.core.FeedbackSessionLogsLogic; import teammates.sqllogic.core.FeedbackSessionsLogic; import teammates.sqllogic.core.NotificationsLogic; import teammates.sqllogic.core.UsageStatisticsLogic; @@ -41,6 +43,7 @@ import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.FeedbackResponseComment; import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; import teammates.storage.sqlentity.Instructor; import teammates.storage.sqlentity.Notification; import teammates.storage.sqlentity.Section; @@ -68,6 +71,7 @@ public class Logic { final FeedbackResponsesLogic feedbackResponsesLogic = FeedbackResponsesLogic.inst(); final FeedbackResponseCommentsLogic feedbackResponseCommentsLogic = FeedbackResponseCommentsLogic.inst(); final FeedbackSessionsLogic feedbackSessionsLogic = FeedbackSessionsLogic.inst(); + final FeedbackSessionLogsLogic feedbackSessionLogsLogic = FeedbackSessionLogsLogic.inst(); final UsageStatisticsLogic usageStatisticsLogic = UsageStatisticsLogic.inst(); final UsersLogic usersLogic = UsersLogic.inst(); final NotificationsLogic notificationsLogic = NotificationsLogic.inst(); @@ -88,32 +92,41 @@ public static Logic inst() { * @throws InvalidParametersException if the account request details are invalid. * @throws EntityAlreadyExistsException if the account request already exists. */ - public AccountRequest createAccountRequest(String name, String email, String institute) - throws InvalidParametersException, EntityAlreadyExistsException { + public AccountRequest createAccountRequest(String name, String email, String institute, AccountRequestStatus status, + String comments) throws InvalidParametersException { - return accountRequestLogic.createAccountRequest(name, email, institute); + return accountRequestLogic.createAccountRequest(name, email, institute, status, comments); } /** - * Creates a or gets an account request. + * Gets the account request with the given {@code id}. * - * @return newly created account request. - * @throws InvalidParametersException if the account request details are invalid. - * @throws EntityAlreadyExistsException if the account request already exists. + * @return account request with the given {@code id}. */ - public AccountRequest createAccountRequestWithTransaction(String name, String email, String institute) - throws InvalidParametersException { + public AccountRequest getAccountRequest(UUID id) { + return accountRequestLogic.getAccountRequest(id); + } - return accountRequestLogic.createOrGetAccountRequestWithTransaction(name, email, institute); + /** + * Gets the account request with the given {@code id}. + * + * @return account request with the given {@code id}. + */ + public AccountRequest getAccountRequestWithTransaction(UUID id) { + return accountRequestLogic.getAccountRequestWithTransaction(id); } /** - * Gets the account request with the given email and institute. + * Creates a or gets an account request. * - * @return account request with the given email and institute. + * @return newly created account request. + * @throws InvalidParametersException if the account request details are invalid. + * @throws EntityAlreadyExistsException if the account request already exists. */ - public AccountRequest getAccountRequest(String email, String institute) { - return accountRequestLogic.getAccountRequest(email, institute); + public AccountRequest createAccountRequestWithTransaction(String name, String email, String institute, + AccountRequestStatus status, String comments) throws InvalidParametersException { + + return accountRequestLogic.createOrGetAccountRequestWithTransaction(name, email, institute, status, comments); } /** @@ -136,19 +149,29 @@ public AccountRequest updateAccountRequest(AccountRequest accountRequest) } /** - * Creates/Resets the account request with the given email and institute + * Updates the given account request. + * + * @return the updated account request. + */ + public AccountRequest updateAccountRequestWithTransaction(AccountRequest accountRequest) + throws InvalidParametersException, EntityDoesNotExistException { + return accountRequestLogic.updateAccountRequestWithTransaction(accountRequest); + } + + /** + * Creates/Resets the account request with the given id * such that it is not registered. * * @return account request that is unregistered with the - * email and institute. + * id. */ - public AccountRequest resetAccountRequest(String email, String institute) + public AccountRequest resetAccountRequest(UUID id) throws EntityDoesNotExistException, InvalidParametersException { - return accountRequestLogic.resetAccountRequest(email, institute); + return accountRequestLogic.resetAccountRequest(id); } /** - * Deletes account request by email and institute. + * Deletes account request by id. * *

    *
  • Fails silently if no such account request.
  • @@ -157,8 +180,29 @@ public AccountRequest resetAccountRequest(String email, String institute) *

    Preconditions:

    * All parameters are non-null. */ - public void deleteAccountRequest(String email, String institute) { - accountRequestLogic.deleteAccountRequest(email, institute); + public void deleteAccountRequest(UUID id) { + accountRequestLogic.deleteAccountRequest(id); + } + + /** + * Gets all pending account requests. + */ + public List getPendingAccountRequests() { + return accountRequestLogic.getPendingAccountRequests(); + } + + /** + * Gets all pending account requests. + */ + public List getAllAccountRequests() { + return accountRequestLogic.getAllAccountRequests(); + } + + /** + * Get a list of account requests associated with email provided. + */ + public List getApprovedAccountRequestsForEmailWithTransaction(String email) { + return accountRequestLogic.getApprovedAccountRequestsForEmailWithTransaction(email); } /** @@ -182,6 +226,13 @@ public List getAccountsForEmail(String email) { return accountsLogic.getAccountsForEmail(email); } + /** + * Get a list of accounts associated with email provided. + */ + public List getAccountsForEmailWithTransaction(String email) { + return accountsLogic.getAccountsForEmailWithTransaction(email); + } + /** * Creates an account. * @@ -194,6 +245,18 @@ public Account createAccount(Account account) return accountsLogic.createAccount(account); } + /** + * Creates an account. + * + * @return the created account + * @throws InvalidParametersException if the account is not valid + * @throws EntityAlreadyExistsException if the account already exists in the database. + */ + public Account createAccountWithTransaction(Account account) + throws InvalidParametersException, EntityAlreadyExistsException { + return accountsLogic.createAccountWithTransaction(account); + } + /** * Deletes account by googleId. * @@ -455,6 +518,15 @@ public FeedbackSession getFeedbackSession(String feedbackSessionName, String cou return feedbackSessionsLogic.getFeedbackSession(feedbackSessionName, courseId); } + /** + * Gets a feedback session reference. + * + * @return Returns a proxy for the feedback session. + */ + public FeedbackSession getFeedbackSessionReference(UUID id) { + return feedbackSessionsLogic.getFeedbackSessionReference(id); + } + /** * Gets a feedback session from the recycle bin. * @@ -604,15 +676,15 @@ public void deleteFeedbackSessionCascade(String feedbackSessionName, String cour } /** - * Soft-deletes a specific session to Recycle Bin. + * Soft-deletes a specific session to Recycle Bin. */ - public void moveFeedbackSessionToRecycleBin(String feedbackSessionName, String courseId) + public FeedbackSession moveFeedbackSessionToRecycleBin(String feedbackSessionName, String courseId) throws EntityDoesNotExistException { assert feedbackSessionName != null; assert courseId != null; - feedbackSessionsLogic.moveFeedbackSessionToRecycleBin(feedbackSessionName, courseId); + return feedbackSessionsLogic.moveFeedbackSessionToRecycleBin(feedbackSessionName, courseId); } /** @@ -936,6 +1008,16 @@ public Student getStudent(UUID id) { return usersLogic.getStudent(id); } + /** + * Gets student reference associated with {@code id}. + * + * @param id Id of Student. + * @return Returns a proxy for the Student. + */ + public Student getStudentReference(UUID id) { + return usersLogic.getStudentReference(id); + } + /** * Gets student associated with {@code courseId} and {@code email}. */ @@ -1608,4 +1690,25 @@ public List getFeedbackSessionsClosingWithinTimeLimit() { public List getFeedbackSessionsOpeningWithinTimeLimit() { return feedbackSessionsLogic.getFeedbackSessionsOpeningWithinTimeLimit(); } + + /** + * Create feedback session logs. + */ + public void createFeedbackSessionLogs(List feedbackSessionLogs) { + feedbackSessionLogsLogic.createFeedbackSessionLogs(feedbackSessionLogs); + } + + /** + * Gets the feedback session logs as filtered by the given parameters ordered by + * ascending timestamp. Logs with the same timestamp will be ordered by the + * student's email. + * + * @param studentId Can be null + * @param feedbackSessionId Can be null + */ + public List getOrderedFeedbackSessionLogs(String courseId, UUID studentId, + UUID feedbackSessionId, Instant startTime, Instant endTime) { + return feedbackSessionLogsLogic.getOrderedFeedbackSessionLogs(courseId, studentId, feedbackSessionId, startTime, + endTime); + } } diff --git a/src/main/java/teammates/sqllogic/api/SqlEmailGenerator.java b/src/main/java/teammates/sqllogic/api/SqlEmailGenerator.java index 2bd8f2bfb08d..cc5fc4507e7d 100644 --- a/src/main/java/teammates/sqllogic/api/SqlEmailGenerator.java +++ b/src/main/java/teammates/sqllogic/api/SqlEmailGenerator.java @@ -26,6 +26,7 @@ import teammates.sqllogic.core.FeedbackSessionsLogic; import teammates.sqllogic.core.UsersLogic; import teammates.storage.sqlentity.Account; +import teammates.storage.sqlentity.AccountRequest; import teammates.storage.sqlentity.Course; import teammates.storage.sqlentity.DeadlineExtension; import teammates.storage.sqlentity.FeedbackSession; @@ -256,12 +257,10 @@ public EmailWrapper generateFeedbackSessionSummaryOfCourse( Course course = coursesLogic.getCourse(courseId); boolean isInstructor = emailType == EmailType.INSTRUCTOR_COURSE_LINKS_REGENERATED; - Student student = null; + Student student = usersLogic.getStudentForEmail(courseId, userEmail); Instructor instructor = null; if (isInstructor) { instructor = usersLogic.getInstructorForEmail(courseId, userEmail); - } else { - student = usersLogic.getStudentForEmail(courseId, userEmail); } List sessions = new ArrayList<>(); @@ -868,11 +867,11 @@ private EmailWrapper generateFeedbackSessionEmailBaseForNotifiedInstructors( } private boolean isYetToJoinCourse(Student student) { - return student.getAccount().getGoogleId() == null || student.getAccount().getGoogleId().isEmpty(); + return student.getAccount() == null || student.getAccount().getGoogleId().isEmpty(); } private boolean isYetToJoinCourse(Instructor instructor) { - return instructor.getAccount().getGoogleId() == null || instructor.getAccount().getGoogleId().isEmpty(); + return instructor.getAccount() == null || instructor.getAccount().getGoogleId().isEmpty(); } /** @@ -973,6 +972,74 @@ public EmailWrapper generateInstructorCourseRejoinEmailAfterGoogleIdReset( return email; } + /** + * Generates the email to alert the admin of the new {@code accountRequest}. + */ + public EmailWrapper generateNewAccountRequestAdminAlertEmail(AccountRequest accountRequest) { + String name = accountRequest.getName(); + String institute = accountRequest.getInstitute(); + String emailAddress = accountRequest.getEmail(); + String comments = accountRequest.getComments(); + if (comments == null) { + comments = ""; + } + String adminAccountRequestsPageUrl = Config.getFrontEndAppUrl(Const.WebPageURIs.ADMIN_HOME_PAGE).toAbsoluteString(); + String[] templateKeyValuePairs = new String[] { + "${name}", name, + "${institute}", institute, + "${emailAddress}", emailAddress, + "${comments}", comments, + "${adminAccountRequestsPageUrl}", adminAccountRequestsPageUrl, + }; + String content = Templates.populateTemplate(EmailTemplates.ADMIN_NEW_ACCOUNT_REQUEST_ALERT, templateKeyValuePairs); + EmailWrapper email = getEmptyEmailAddressedToEmail(Config.SUPPORT_EMAIL); + email.setType(EmailType.NEW_ACCOUNT_REQUEST_ADMIN_ALERT); + email.setSubjectFromType(); + email.setContent(content); + return email; + } + + /** + * Generates the acknowledgement email to be sent to the person who submitted {@code accountRequest}. + */ + public EmailWrapper generateNewAccountRequestAcknowledgementEmail(AccountRequest accountRequest) { + String name = SanitizationHelper.sanitizeForHtml(accountRequest.getName()); + String institute = SanitizationHelper.sanitizeForHtml(accountRequest.getInstitute()); + String emailAddress = SanitizationHelper.sanitizeForHtml(accountRequest.getEmail()); + String comments = SanitizationHelper.sanitizeForHtml(accountRequest.getComments()); + if (comments == null) { + comments = ""; + } + String[] templateKeyValuePairs = new String[] { + "${name}", name, + "${institute}", institute, + "${emailAddress}", emailAddress, + "${comments}", comments, + "${supportEmail}", Config.SUPPORT_EMAIL, + }; + String content = Templates.populateTemplate( + EmailTemplates.INSTRUCTOR_NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT, templateKeyValuePairs); + EmailWrapper email = getEmptyEmailAddressedToEmail(emailAddress); + email.setType(EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT); + email.setBcc(Config.SUPPORT_EMAIL); + email.setSubjectFromType(); + email.setContent(content); + return email; + } + + /** + * Generates the email to be sent to instructor when their account request has been rejected by admin. + */ + public EmailWrapper generateAccountRequestRejectionEmail(AccountRequest accountRequest, String title, String content) { + EmailWrapper email = getEmptyEmailAddressedToEmail(accountRequest.getEmail()); + email.setType(EmailType.ACCOUNT_REQUEST_REJECTION); + email.setBcc(Config.SUPPORT_EMAIL); + email.setSubjectFromType(SanitizationHelper.sanitizeTitle(title)); + email.setContent(SanitizationHelper.sanitizeForRichText(content)); + + return email; + } + /** * Generates the course registered email for the user with the given details in {@code course}. */ diff --git a/src/main/java/teammates/sqllogic/core/AccountRequestsLogic.java b/src/main/java/teammates/sqllogic/core/AccountRequestsLogic.java index f0797adf0345..996e52abb0ab 100644 --- a/src/main/java/teammates/sqllogic/core/AccountRequestsLogic.java +++ b/src/main/java/teammates/sqllogic/core/AccountRequestsLogic.java @@ -1,8 +1,9 @@ package teammates.sqllogic.core; import java.util.List; +import java.util.UUID; -import teammates.common.exception.EntityAlreadyExistsException; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; import teammates.common.exception.SearchServiceException; @@ -52,27 +53,35 @@ public void putDocument(AccountRequest accountRequest) throws SearchServiceExcep /** * Creates an account request. */ - public AccountRequest createAccountRequest(AccountRequest accountRequest) - throws InvalidParametersException, EntityAlreadyExistsException { + public AccountRequest createAccountRequest(AccountRequest accountRequest) throws InvalidParametersException { return accountRequestDb.createAccountRequest(accountRequest); } /** * Creates an account request. */ - public AccountRequest createAccountRequest(String name, String email, String institute) - throws InvalidParametersException, EntityAlreadyExistsException { - AccountRequest toCreate = new AccountRequest(email, name, institute); + public AccountRequest createAccountRequest(String name, String email, String institute, AccountRequestStatus status, + String comments) throws InvalidParametersException { + AccountRequest toCreate = new AccountRequest(email, name, institute, status, comments); return accountRequestDb.createAccountRequest(toCreate); } /** - * Gets account request associated with the {@code email} and {@code institute}. + * Gets the account request associated with the {@code id}. */ - public AccountRequest getAccountRequest(String email, String institute) { + public AccountRequest getAccountRequest(UUID id) { + return accountRequestDb.getAccountRequest(id); + } - return accountRequestDb.getAccountRequest(email, institute); + /** + * Gets the account request associated with the {@code id}. + */ + public AccountRequest getAccountRequestWithTransaction(UUID id) { + HibernateUtil.beginTransaction(); + AccountRequest request = accountRequestDb.getAccountRequest(id); + HibernateUtil.commitTransaction(); + return request; } /** @@ -83,6 +92,27 @@ public AccountRequest updateAccountRequest(AccountRequest accountRequest) return accountRequestDb.updateAccountRequest(accountRequest); } + /** + * Updates an account request. + */ + @SuppressWarnings("PMD") + public AccountRequest updateAccountRequestWithTransaction(AccountRequest accountRequest) + throws InvalidParametersException, EntityDoesNotExistException { + + HibernateUtil.beginTransaction(); + AccountRequest updatedRequest; + + try { + updatedRequest = accountRequestDb.updateAccountRequest(accountRequest); + HibernateUtil.commitTransaction(); + } catch (InvalidParametersException ipe) { + HibernateUtil.rollbackTransaction(); + throw new InvalidParametersException(ipe.getMessage()); + } + + return updatedRequest; + } + /** * Gets account request associated with the {@code regkey}. */ @@ -91,15 +121,39 @@ public AccountRequest getAccountRequestByRegistrationKey(String regkey) { } /** - * Creates/resets the account request with the given email and institute such that it is not registered. + * Gets all pending account requests. + */ + public List getPendingAccountRequests() { + return accountRequestDb.getPendingAccountRequests(); + } + + /** + * Gets all account requests. + */ + public List getAllAccountRequests() { + return accountRequestDb.getAllAccountRequests(); + } + + /** + * Get a list of account requests associated with email provided. + */ + public List getApprovedAccountRequestsForEmailWithTransaction(String email) { + HibernateUtil.beginTransaction(); + List accountRequests = accountRequestDb.getApprovedAccountRequestsForEmail(email); + HibernateUtil.commitTransaction(); + return accountRequests; + } + + /** + * Creates/resets the account request with the given id such that it is not registered. */ - public AccountRequest resetAccountRequest(String email, String institute) + public AccountRequest resetAccountRequest(UUID id) throws EntityDoesNotExistException, InvalidParametersException { - AccountRequest accountRequest = accountRequestDb.getAccountRequest(email, institute); + AccountRequest accountRequest = accountRequestDb.getAccountRequest(id); if (accountRequest == null) { throw new EntityDoesNotExistException("Failed to reset since AccountRequest with " - + "the given email and institute cannot be found."); + + "the given id cannot be found."); } accountRequest.setRegisteredAt(null); @@ -107,13 +161,13 @@ public AccountRequest resetAccountRequest(String email, String institute) } /** - * Deletes account request associated with the {@code email} and {@code institute}. + * Deletes account request associated with the {@code id}. * - *

    Fails silently if no account requests with the given email and institute to delete can be found.

    + *

    Fails silently if no account requests with the given id to delete can be found.

    * */ - public void deleteAccountRequest(String email, String institute) { - AccountRequest toDelete = accountRequestDb.getAccountRequest(email, institute); + public void deleteAccountRequest(UUID id) { + AccountRequest toDelete = accountRequestDb.getAccountRequest(id); accountRequestDb.deleteAccountRequest(toDelete); } @@ -131,9 +185,10 @@ public List searchAccountRequestsInWholeSystem(String queryStrin /** * Creates an or gets an account request. */ - public AccountRequest createOrGetAccountRequestWithTransaction(String name, String email, String institute) + public AccountRequest createOrGetAccountRequestWithTransaction(String name, String email, String institute, + AccountRequestStatus status, String comments) throws InvalidParametersException { - AccountRequest toCreate = new AccountRequest(email, name, institute); + AccountRequest toCreate = new AccountRequest(email, name, institute, status, comments); HibernateUtil.beginTransaction(); AccountRequest accountRequest; try { @@ -142,10 +197,6 @@ public AccountRequest createOrGetAccountRequestWithTransaction(String name, Stri } catch (InvalidParametersException ipe) { HibernateUtil.rollbackTransaction(); throw new InvalidParametersException(ipe); - } catch (EntityAlreadyExistsException eaee) { - // Use existing account request - accountRequest = getAccountRequest(email, institute); - HibernateUtil.commitTransaction(); } return accountRequest; } diff --git a/src/main/java/teammates/sqllogic/core/AccountsLogic.java b/src/main/java/teammates/sqllogic/core/AccountsLogic.java index 74bc4af732bb..499480804482 100644 --- a/src/main/java/teammates/sqllogic/core/AccountsLogic.java +++ b/src/main/java/teammates/sqllogic/core/AccountsLogic.java @@ -8,6 +8,7 @@ import teammates.common.exception.EntityAlreadyExistsException; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; +import teammates.common.util.HibernateUtil; import teammates.storage.sqlapi.AccountsDb; import teammates.storage.sqlentity.Account; import teammates.storage.sqlentity.Course; @@ -77,6 +78,19 @@ public List getAccountsForEmail(String email) { return accountsDb.getAccountsByEmail(email); } + /** + * Gets accounts associated with email. + */ + public List getAccountsForEmailWithTransaction(String email) { + assert email != null; + + HibernateUtil.beginTransaction(); + List accounts = accountsDb.getAccountsByEmail(email); + HibernateUtil.commitTransaction(); + + return accounts; + } + /** * Creates an account. * @@ -91,6 +105,25 @@ public Account createAccount(Account account) return accountsDb.createAccount(account); } + /** + * Creates an account. + * + * @return the created account + * @throws InvalidParametersException if the account is not valid + * @throws EntityAlreadyExistsException if the account already exists in the + * database. + */ + public Account createAccountWithTransaction(Account account) + throws InvalidParametersException, EntityAlreadyExistsException { + assert account != null; + + HibernateUtil.beginTransaction(); + Account createdAccount = accountsDb.createAccount(account); + HibernateUtil.commitTransaction(); + + return createdAccount; + } + /** * Deletes account associated with the {@code googleId}. * diff --git a/src/main/java/teammates/sqllogic/core/DataBundleLogic.java b/src/main/java/teammates/sqllogic/core/DataBundleLogic.java index 8ab165d31b72..0bb79e0ff72c 100644 --- a/src/main/java/teammates/sqllogic/core/DataBundleLogic.java +++ b/src/main/java/teammates/sqllogic/core/DataBundleLogic.java @@ -1,5 +1,6 @@ package teammates.sqllogic.core; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -19,6 +20,7 @@ import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.FeedbackResponseComment; import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; import teammates.storage.sqlentity.Instructor; import teammates.storage.sqlentity.Notification; import teammates.storage.sqlentity.ReadNotification; @@ -41,6 +43,7 @@ public final class DataBundleLogic { private CoursesLogic coursesLogic; private DeadlineExtensionsLogic deadlineExtensionsLogic; private FeedbackSessionsLogic fsLogic; + private FeedbackSessionLogsLogic fslLogic; private FeedbackQuestionsLogic fqLogic; private FeedbackResponsesLogic frLogic; private FeedbackResponseCommentsLogic frcLogic; @@ -56,16 +59,15 @@ public static DataBundleLogic inst() { } void initLogicDependencies(AccountsLogic accountsLogic, AccountRequestsLogic accountRequestsLogic, - CoursesLogic coursesLogic, - DeadlineExtensionsLogic deadlineExtensionsLogic, FeedbackSessionsLogic fsLogic, - FeedbackQuestionsLogic fqLogic, FeedbackResponsesLogic frLogic, - FeedbackResponseCommentsLogic frcLogic, - NotificationsLogic notificationsLogic, UsersLogic usersLogic) { + CoursesLogic coursesLogic, DeadlineExtensionsLogic deadlineExtensionsLogic, FeedbackSessionsLogic fsLogic, + FeedbackSessionLogsLogic fslLogic, FeedbackQuestionsLogic fqLogic, FeedbackResponsesLogic frLogic, + FeedbackResponseCommentsLogic frcLogic, NotificationsLogic notificationsLogic, UsersLogic usersLogic) { this.accountsLogic = accountsLogic; this.accountRequestsLogic = accountRequestsLogic; this.coursesLogic = coursesLogic; this.deadlineExtensionsLogic = deadlineExtensionsLogic; this.fsLogic = fsLogic; + this.fslLogic = fslLogic; this.fqLogic = fqLogic; this.frLogic = frLogic; this.frcLogic = frcLogic; @@ -97,6 +99,7 @@ public static SqlDataBundle deserializeDataBundle(String jsonString) { Collection instructors = dataBundle.instructors.values(); Collection students = dataBundle.students.values(); Collection sessions = dataBundle.feedbackSessions.values(); + Collection sessionLogs = dataBundle.feedbackSessionLogs.values(); Collection questions = dataBundle.feedbackQuestions.values(); Collection responses = dataBundle.feedbackResponses.values(); Collection responseComments = dataBundle.feedbackResponseComments.values(); @@ -215,6 +218,14 @@ public static SqlDataBundle deserializeDataBundle(String jsonString) { student.generateNewRegistrationKey(); } + for (FeedbackSessionLog log : sessionLogs) { + log.setId(UUID.randomUUID()); + FeedbackSession fs = sessionsMap.get(log.getFeedbackSession().getId()); + log.setFeedbackSession(fs); + Student student = (Student) usersMap.get(log.getStudent().getId()); + log.setStudent(student); + } + for (Notification notification : notifications) { UUID placeholderId = notification.getId(); notification.setId(UUID.randomUUID()); @@ -264,6 +275,7 @@ public SqlDataBundle persistDataBundle(SqlDataBundle dataBundle) Collection instructors = dataBundle.instructors.values(); Collection students = dataBundle.students.values(); Collection sessions = dataBundle.feedbackSessions.values(); + Collection sessionLogs = dataBundle.feedbackSessionLogs.values(); Collection questions = dataBundle.feedbackQuestions.values(); Collection responses = dataBundle.feedbackResponses.values(); Collection responseComments = dataBundle.feedbackResponseComments.values(); @@ -320,6 +332,8 @@ public SqlDataBundle persistDataBundle(SqlDataBundle dataBundle) usersLogic.createStudent(student); } + fslLogic.createFeedbackSessionLogs(new ArrayList<>(sessionLogs)); + for (ReadNotification readNotification : readNotifications) { accountsLogic.updateReadNotifications(readNotification.getAccount().getGoogleId(), readNotification.getNotification().getId(), readNotification.getNotification().getEndTime()); @@ -351,7 +365,7 @@ public void removeDataBundle(SqlDataBundle dataBundle) throws InvalidParametersE accountsLogic.deleteAccount(account.getGoogleId()); }); dataBundle.accountRequests.values().forEach(accountRequest -> { - accountRequestsLogic.deleteAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + accountRequestsLogic.deleteAccountRequest(accountRequest.getId()); }); } diff --git a/src/main/java/teammates/sqllogic/core/FeedbackQuestionsLogic.java b/src/main/java/teammates/sqllogic/core/FeedbackQuestionsLogic.java index 02dd878541c3..7ccf253a0e02 100644 --- a/src/main/java/teammates/sqllogic/core/FeedbackQuestionsLogic.java +++ b/src/main/java/teammates/sqllogic/core/FeedbackQuestionsLogic.java @@ -10,7 +10,7 @@ import java.util.UUID; import java.util.stream.Collectors; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.FeedbackQuestionRecipient; diff --git a/src/main/java/teammates/sqllogic/core/FeedbackResponseCommentsLogic.java b/src/main/java/teammates/sqllogic/core/FeedbackResponseCommentsLogic.java index 7883c7e5432d..e94c0ea45388 100644 --- a/src/main/java/teammates/sqllogic/core/FeedbackResponseCommentsLogic.java +++ b/src/main/java/teammates/sqllogic/core/FeedbackResponseCommentsLogic.java @@ -4,7 +4,7 @@ import java.util.Set; import java.util.UUID; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.SqlCourseRoster; diff --git a/src/main/java/teammates/sqllogic/core/FeedbackResponsesLogic.java b/src/main/java/teammates/sqllogic/core/FeedbackResponsesLogic.java index aef4f49967d8..bd2b6b430722 100644 --- a/src/main/java/teammates/sqllogic/core/FeedbackResponsesLogic.java +++ b/src/main/java/teammates/sqllogic/core/FeedbackResponsesLogic.java @@ -9,7 +9,7 @@ import java.util.Set; import java.util.UUID; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.FeedbackResultFetchType; @@ -344,7 +344,7 @@ public void updateRankRecipientQuestionResponsesAfterDeletingStudent(String cour */ private void makeRankRecipientQuestionResponsesConsistent( FeedbackQuestion question, SqlCourseRoster roster) { - assert !question.getQuestionDetailsCopy().getQuestionType() + assert question.getQuestionDetailsCopy().getQuestionType() .equals(FeedbackQuestionType.RANK_RECIPIENTS); FeedbackParticipantType giverType = question.getGiverType(); @@ -593,7 +593,7 @@ private SqlSessionResultsBundle buildResultsBundle( Set studentsEmailInTeam = new HashSet<>(); if (student != null) { for (Student studentInTeam - : roster.getTeamToMembersTable().getOrDefault(student.getTeam(), Collections.emptyList())) { + : roster.getTeamToMembersTable().getOrDefault(student.getTeam().getName(), Collections.emptyList())) { studentsEmailInTeam.add(studentInTeam.getEmail()); } } diff --git a/src/main/java/teammates/sqllogic/core/FeedbackSessionLogsLogic.java b/src/main/java/teammates/sqllogic/core/FeedbackSessionLogsLogic.java new file mode 100644 index 000000000000..8ea0c4f3fe50 --- /dev/null +++ b/src/main/java/teammates/sqllogic/core/FeedbackSessionLogsLogic.java @@ -0,0 +1,67 @@ +package teammates.sqllogic.core; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +import org.hibernate.ObjectNotFoundException; + +import teammates.common.util.Logger; +import teammates.storage.sqlapi.FeedbackSessionLogsDb; +import teammates.storage.sqlentity.FeedbackSessionLog; + +/** + * Handles operations related to feedback sessions. + * + * @see FeedbackSessionLog + * @see FeedbackSessionLogsDb + */ +public final class FeedbackSessionLogsLogic { + + private static final Logger log = Logger.getLogger(); + + private static final FeedbackSessionLogsLogic instance = new FeedbackSessionLogsLogic(); + + private static final String ERROR_FAILED_TO_CREATE_LOG = "Failed to create session activity log"; + + private FeedbackSessionLogsDb fslDb; + + private FeedbackSessionLogsLogic() { + // prevent initialization + } + + public static FeedbackSessionLogsLogic inst() { + return instance; + } + + void initLogicDependencies(FeedbackSessionLogsDb fslDb) { + this.fslDb = fslDb; + } + + /** + * Creates feedback session logs. + */ + public void createFeedbackSessionLogs(List fsLogs) { + for (FeedbackSessionLog fsLog : fsLogs) { + try { + fslDb.createFeedbackSessionLog(fsLog); + } catch (ObjectNotFoundException e) { + log.severe(String.format(ERROR_FAILED_TO_CREATE_LOG), e); + } + } + } + + /** + * Gets the feedback session logs as filtered by the given parameters ordered by + * ascending timestamp. Logs with the same timestamp will be ordered by the + * student's email. + * + * @param studentId Can be null + * @param feedbackSessionId Can be null + */ + public List getOrderedFeedbackSessionLogs(String courseId, UUID studentId, + UUID feedbackSessionId, Instant startTime, Instant endTime) { + return fslDb.getOrderedFeedbackSessionLogs(courseId, studentId, feedbackSessionId, startTime, + endTime); + } +} diff --git a/src/main/java/teammates/sqllogic/core/FeedbackSessionsLogic.java b/src/main/java/teammates/sqllogic/core/FeedbackSessionsLogic.java index 84ea61b2a0ce..b634d441e6a1 100644 --- a/src/main/java/teammates/sqllogic/core/FeedbackSessionsLogic.java +++ b/src/main/java/teammates/sqllogic/core/FeedbackSessionsLogic.java @@ -87,6 +87,16 @@ public FeedbackSession getFeedbackSession(String feedbackSessionName, String cou return fsDb.getFeedbackSession(feedbackSessionName, courseId); } + /** + * Gets a feedback session reference. + * + * @return Returns a proxy for the feedback session. + */ + public FeedbackSession getFeedbackSessionReference(UUID id) { + assert id != null; + return fsDb.getFeedbackSessionReference(id); + } + /** * Gets all feedback sessions of a course, except those that are soft-deleted. */ @@ -111,7 +121,7 @@ public List getFeedbackSessionsForCourseStartingAfter(String co * @return null if not found. */ public FeedbackSession getFeedbackSessionFromRecycleBin(String feedbackSessionName, String courseId) { - return fsDb.getSoftDeletedFeedbackSession(courseId, feedbackSessionName); + return fsDb.getSoftDeletedFeedbackSession(feedbackSessionName, courseId); } /** @@ -281,9 +291,9 @@ public void deleteFeedbackSessionCascade(String feedbackSessionName, String cour /** * Soft-deletes a specific feedback session to Recycle Bin. - * @return the time when the feedback session is moved to the recycle bin + * @return the feedback session */ - public Instant moveFeedbackSessionToRecycleBin(String feedbackSessionName, String courseId) + public FeedbackSession moveFeedbackSessionToRecycleBin(String feedbackSessionName, String courseId) throws EntityDoesNotExistException { return fsDb.softDeleteFeedbackSession(feedbackSessionName, courseId); diff --git a/src/main/java/teammates/sqllogic/core/LogicStarter.java b/src/main/java/teammates/sqllogic/core/LogicStarter.java index b474cac89808..a5ca2ccd520f 100644 --- a/src/main/java/teammates/sqllogic/core/LogicStarter.java +++ b/src/main/java/teammates/sqllogic/core/LogicStarter.java @@ -1,7 +1,7 @@ package teammates.sqllogic.core; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; import teammates.common.util.Logger; import teammates.storage.sqlapi.AccountRequestsDb; @@ -11,6 +11,7 @@ import teammates.storage.sqlapi.FeedbackQuestionsDb; import teammates.storage.sqlapi.FeedbackResponseCommentsDb; import teammates.storage.sqlapi.FeedbackResponsesDb; +import teammates.storage.sqlapi.FeedbackSessionLogsDb; import teammates.storage.sqlapi.FeedbackSessionsDb; import teammates.storage.sqlapi.NotificationsDb; import teammates.storage.sqlapi.UsageStatisticsDb; @@ -33,6 +34,7 @@ public static void initializeDependencies() { DataBundleLogic dataBundleLogic = DataBundleLogic.inst(); DeadlineExtensionsLogic deadlineExtensionsLogic = DeadlineExtensionsLogic.inst(); FeedbackSessionsLogic fsLogic = FeedbackSessionsLogic.inst(); + FeedbackSessionLogsLogic fslLogic = FeedbackSessionLogsLogic.inst(); FeedbackResponsesLogic frLogic = FeedbackResponsesLogic.inst(); FeedbackResponseCommentsLogic frcLogic = FeedbackResponseCommentsLogic.inst(); FeedbackQuestionsLogic fqLogic = FeedbackQuestionsLogic.inst(); @@ -44,10 +46,11 @@ public static void initializeDependencies() { accountsLogic.initLogicDependencies(AccountsDb.inst(), notificationsLogic, usersLogic, coursesLogic); coursesLogic.initLogicDependencies(CoursesDb.inst(), fsLogic, usersLogic); dataBundleLogic.initLogicDependencies(accountsLogic, accountRequestsLogic, coursesLogic, - deadlineExtensionsLogic, fsLogic, fqLogic, frLogic, frcLogic, + deadlineExtensionsLogic, fsLogic, fslLogic, fqLogic, frLogic, frcLogic, notificationsLogic, usersLogic); deadlineExtensionsLogic.initLogicDependencies(DeadlineExtensionsDb.inst(), fsLogic); fsLogic.initLogicDependencies(FeedbackSessionsDb.inst(), coursesLogic, frLogic, fqLogic, usersLogic); + fslLogic.initLogicDependencies(FeedbackSessionLogsDb.inst()); frLogic.initLogicDependencies(FeedbackResponsesDb.inst(), usersLogic, fqLogic, frcLogic); frcLogic.initLogicDependencies(FeedbackResponseCommentsDb.inst()); fqLogic.initLogicDependencies(FeedbackQuestionsDb.inst(), coursesLogic, frLogic, usersLogic, fsLogic); diff --git a/src/main/java/teammates/sqllogic/core/UsersLogic.java b/src/main/java/teammates/sqllogic/core/UsersLogic.java index e0486616a691..bfac68a3f36e 100644 --- a/src/main/java/teammates/sqllogic/core/UsersLogic.java +++ b/src/main/java/teammates/sqllogic/core/UsersLogic.java @@ -482,6 +482,18 @@ public Student getStudent(UUID id) { return usersDb.getStudent(id); } + /** + * Gets student reference associated with {@code id}. + * + * @param id Id of Student. + * @return Returns a proxy for the Student. + */ + public Student getStudentReference(UUID id) { + assert id != null; + + return usersDb.getStudentReference(id); + } + /** * Gets the student with the specified email. */ diff --git a/src/main/java/teammates/storage/api/FeedbackQuestionsDb.java b/src/main/java/teammates/storage/api/FeedbackQuestionsDb.java index 1a71a6270f17..dd632fd72091 100644 --- a/src/main/java/teammates/storage/api/FeedbackQuestionsDb.java +++ b/src/main/java/teammates/storage/api/FeedbackQuestionsDb.java @@ -216,13 +216,13 @@ private List getFeedbackQuestionEntitiesForGiverType( private boolean hasFeedbackQuestionEntitiesForGiverType( String feedbackSessionName, String courseId, FeedbackParticipantType giverType) { - return load() + return !load() .filter("feedbackSessionName =", feedbackSessionName) .filter("courseId =", courseId) .filter("giverType =", giverType) .keys() .list() - .size() != 0; + .isEmpty(); } @Override diff --git a/src/main/java/teammates/storage/api/OfyHelper.java b/src/main/java/teammates/storage/api/OfyHelper.java index 7f89b75ed02f..d25af1c0c654 100644 --- a/src/main/java/teammates/storage/api/OfyHelper.java +++ b/src/main/java/teammates/storage/api/OfyHelper.java @@ -1,8 +1,9 @@ package teammates.storage.api; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import com.google.cloud.NoCredentials; import com.google.cloud.datastore.DatastoreOptions; import com.googlecode.objectify.ObjectifyFactory; import com.googlecode.objectify.ObjectifyService; @@ -31,6 +32,7 @@ private static void initializeDatastore() { DatastoreOptions.Builder builder = DatastoreOptions.newBuilder().setProjectId(Config.APP_ID); if (Config.IS_DEV_SERVER) { builder.setHost("http://localhost:" + Config.APP_LOCALDATASTORE_PORT); + builder.setCredentials(NoCredentials.getInstance()); } ObjectifyService.init(new ObjectifyFactory(builder.build().getService())); } diff --git a/src/main/java/teammates/storage/entity/FeedbackSession.java b/src/main/java/teammates/storage/entity/FeedbackSession.java index 06c987af83a3..a2d9674b293e 100644 --- a/src/main/java/teammates/storage/entity/FeedbackSession.java +++ b/src/main/java/teammates/storage/entity/FeedbackSession.java @@ -10,6 +10,8 @@ import com.googlecode.objectify.annotation.Translate; import com.googlecode.objectify.annotation.Unindex; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + /** * Represents an instructor-created Feedback Session. */ @@ -25,6 +27,7 @@ public class FeedbackSession extends BaseEntity { * @see #generateId(String, String) */ @SuppressWarnings({"PMD.UnusedPrivateField", "PMD.SingularField"}) + @SuppressFBWarnings("URF_UNREAD_FIELD") @Id private transient String feedbackSessionId; diff --git a/src/main/java/teammates/storage/search/SearchManagerStarter.java b/src/main/java/teammates/storage/search/SearchManagerStarter.java index f6b413299472..04fc79aa31f0 100644 --- a/src/main/java/teammates/storage/search/SearchManagerStarter.java +++ b/src/main/java/teammates/storage/search/SearchManagerStarter.java @@ -1,7 +1,7 @@ package teammates.storage.search; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; import teammates.common.util.Config; diff --git a/src/main/java/teammates/storage/sqlapi/AccountRequestsDb.java b/src/main/java/teammates/storage/sqlapi/AccountRequestsDb.java index a315cb184843..ed335ae8e932 100644 --- a/src/main/java/teammates/storage/sqlapi/AccountRequestsDb.java +++ b/src/main/java/teammates/storage/sqlapi/AccountRequestsDb.java @@ -1,6 +1,5 @@ package teammates.storage.sqlapi; -import static teammates.common.util.Const.ERROR_CREATE_ENTITY_ALREADY_EXISTS; import static teammates.common.util.Const.ERROR_UPDATE_NON_EXISTENT; import java.time.Instant; @@ -9,7 +8,12 @@ import java.util.List; import java.util.UUID; -import teammates.common.exception.EntityAlreadyExistsException; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; import teammates.common.exception.SearchServiceException; @@ -18,11 +22,6 @@ import teammates.storage.sqlsearch.AccountRequestSearchManager; import teammates.storage.sqlsearch.SearchManagerFactory; -import jakarta.persistence.TypedQuery; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Root; - /** * Generates CRUD operations for AccountRequest. * @@ -46,36 +45,64 @@ public AccountRequestSearchManager getSearchManager() { /** * Creates an AccountRequest in the database. */ - public AccountRequest createAccountRequest(AccountRequest accountRequest) - throws InvalidParametersException, EntityAlreadyExistsException { + public AccountRequest createAccountRequest(AccountRequest accountRequest) throws InvalidParametersException { assert accountRequest != null; if (!accountRequest.isValid()) { throw new InvalidParametersException(accountRequest.getInvalidityInfo()); } - - // don't need to check registrationKey for uniqueness since it is generated using email + institute - if (getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()) != null) { - throw new EntityAlreadyExistsException( - String.format(ERROR_CREATE_ENTITY_ALREADY_EXISTS, accountRequest.toString())); - } - persist(accountRequest); return accountRequest; } /** - * Get AccountRequest by {@code email} and {@code institute} from database. + * Get AccountRequest by {@code id} from the database. + */ + public AccountRequest getAccountRequest(UUID id) { + assert id != null; + return HibernateUtil.get(AccountRequest.class, id); + } + + /** + * Get all Account Requests with {@code status} of 'pending'. */ - public AccountRequest getAccountRequest(String email, String institute) { + public List getPendingAccountRequests() { CriteriaBuilder cb = HibernateUtil.getCriteriaBuilder(); CriteriaQuery cr = cb.createQuery(AccountRequest.class); Root root = cr.from(AccountRequest.class); - cr.select(root).where(cb.and(cb.equal( - root.get("email"), email), cb.equal(root.get("institute"), institute))); + cr.select(root) + .where(cb.equal(root.get("status"), AccountRequestStatus.PENDING)) + .orderBy(cb.desc(root.get("createdAt"))); TypedQuery query = HibernateUtil.createQuery(cr); - return query.getResultStream().findFirst().orElse(null); + return query.getResultList(); + } + + /** + * Get all Account Requests. + */ + public List getAllAccountRequests() { + CriteriaBuilder cb = HibernateUtil.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery(AccountRequest.class); + Root root = cr.from(AccountRequest.class); + cr.select(root); + + TypedQuery query = HibernateUtil.createQuery(cr); + return query.getResultList(); + } + + /** + * Get all Account Requests for a given {@code email}. + */ + public List getApprovedAccountRequestsForEmail(String email) { + CriteriaBuilder cb = HibernateUtil.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery(AccountRequest.class); + Root root = cr.from(AccountRequest.class); + cr.select(root).where(cb.and(cb.equal(root.get("email"), email), + cb.equal(root.get("status"), AccountRequestStatus.APPROVED))); + + TypedQuery query = HibernateUtil.createQuery(cr); + return query.getResultList(); } /** @@ -116,7 +143,7 @@ public AccountRequest updateAccountRequest(AccountRequest accountRequest) throw new InvalidParametersException(accountRequest.getInvalidityInfo()); } - if (getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()) == null) { + if (getAccountRequest(accountRequest.getId()) == null) { throw new EntityDoesNotExistException( String.format(ERROR_UPDATE_NON_EXISTENT, accountRequest.toString())); } @@ -140,10 +167,8 @@ public void deleteAccountRequest(AccountRequest accountRequest) { */ public void deleteDocumentByAccountRequestId(UUID accountRequestId) { if (getSearchManager() != null) { - // Solr saves the id with the prefix "java.util.UUID:", so we need to add it here to - // identify and delete the document from the index getSearchManager().deleteDocuments( - Collections.singletonList("java.util.UUID:" + accountRequestId.toString())); + Collections.singletonList(accountRequestId.toString())); } } diff --git a/src/main/java/teammates/storage/sqlapi/AccountsDb.java b/src/main/java/teammates/storage/sqlapi/AccountsDb.java index 7930d1c28c7c..24a53b53d29b 100644 --- a/src/main/java/teammates/storage/sqlapi/AccountsDb.java +++ b/src/main/java/teammates/storage/sqlapi/AccountsDb.java @@ -6,16 +6,16 @@ import java.util.List; import java.util.UUID; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + import teammates.common.exception.EntityAlreadyExistsException; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; import teammates.common.util.HibernateUtil; import teammates.storage.sqlentity.Account; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Root; - /** * Handles CRUD operations for accounts. * diff --git a/src/main/java/teammates/storage/sqlapi/CoursesDb.java b/src/main/java/teammates/storage/sqlapi/CoursesDb.java index 710eeb04bd9e..f316c06f424c 100644 --- a/src/main/java/teammates/storage/sqlapi/CoursesDb.java +++ b/src/main/java/teammates/storage/sqlapi/CoursesDb.java @@ -6,6 +6,13 @@ import java.util.List; import java.util.UUID; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Root; +import jakarta.persistence.criteria.Subquery; + import teammates.common.exception.EntityAlreadyExistsException; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; @@ -14,13 +21,6 @@ import teammates.storage.sqlentity.Section; import teammates.storage.sqlentity.Team; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaDelete; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Join; -import jakarta.persistence.criteria.Root; -import jakarta.persistence.criteria.Subquery; - /** * Handles CRUD operations for courses. * diff --git a/src/main/java/teammates/storage/sqlapi/DeadlineExtensionsDb.java b/src/main/java/teammates/storage/sqlapi/DeadlineExtensionsDb.java index c1042ea2abf6..7526033dd53c 100644 --- a/src/main/java/teammates/storage/sqlapi/DeadlineExtensionsDb.java +++ b/src/main/java/teammates/storage/sqlapi/DeadlineExtensionsDb.java @@ -7,6 +7,12 @@ import java.util.List; import java.util.UUID; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Root; + import teammates.common.exception.EntityAlreadyExistsException; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; @@ -16,12 +22,6 @@ import teammates.storage.sqlentity.FeedbackSession; import teammates.storage.sqlentity.User; -import jakarta.persistence.TypedQuery; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Join; -import jakarta.persistence.criteria.Root; - /** * Handles CRUD operations for deadline extensions. * diff --git a/src/main/java/teammates/storage/sqlapi/FeedbackQuestionsDb.java b/src/main/java/teammates/storage/sqlapi/FeedbackQuestionsDb.java index 5a765210251b..ba4ac18b3949 100644 --- a/src/main/java/teammates/storage/sqlapi/FeedbackQuestionsDb.java +++ b/src/main/java/teammates/storage/sqlapi/FeedbackQuestionsDb.java @@ -5,6 +5,11 @@ import java.util.List; import java.util.UUID; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Root; + import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.exception.EntityAlreadyExistsException; import teammates.common.exception.InvalidParametersException; @@ -13,11 +18,6 @@ import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackSession; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Join; -import jakarta.persistence.criteria.Root; - /** * Handles CRUD operations for feedback questions. * diff --git a/src/main/java/teammates/storage/sqlapi/FeedbackResponseCommentsDb.java b/src/main/java/teammates/storage/sqlapi/FeedbackResponseCommentsDb.java index cd0f59c58c8b..09b56bacab86 100644 --- a/src/main/java/teammates/storage/sqlapi/FeedbackResponseCommentsDb.java +++ b/src/main/java/teammates/storage/sqlapi/FeedbackResponseCommentsDb.java @@ -6,6 +6,11 @@ import java.util.List; import java.util.UUID; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Root; + import teammates.common.exception.EntityAlreadyExistsException; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; @@ -17,11 +22,6 @@ import teammates.storage.sqlentity.FeedbackSession; import teammates.storage.sqlentity.Section; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Join; -import jakarta.persistence.criteria.Root; - /** * Handles CRUD operations for feedbackResponseComments. * diff --git a/src/main/java/teammates/storage/sqlapi/FeedbackResponsesDb.java b/src/main/java/teammates/storage/sqlapi/FeedbackResponsesDb.java index 7c79986719fd..5dcf7ab375e8 100644 --- a/src/main/java/teammates/storage/sqlapi/FeedbackResponsesDb.java +++ b/src/main/java/teammates/storage/sqlapi/FeedbackResponsesDb.java @@ -6,6 +6,14 @@ import java.util.List; import java.util.UUID; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import jakarta.persistence.criteria.Subquery; + import teammates.common.datatransfer.FeedbackResultFetchType; import teammates.common.exception.EntityAlreadyExistsException; import teammates.common.exception.EntityDoesNotExistException; @@ -17,14 +25,6 @@ import teammates.storage.sqlentity.FeedbackSession; import teammates.storage.sqlentity.Section; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaDelete; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Join; -import jakarta.persistence.criteria.Predicate; -import jakarta.persistence.criteria.Root; -import jakarta.persistence.criteria.Subquery; - /** * Handles CRUD operations for feedbackResponses. * diff --git a/src/main/java/teammates/storage/sqlapi/FeedbackSessionLogsDb.java b/src/main/java/teammates/storage/sqlapi/FeedbackSessionLogsDb.java new file mode 100644 index 000000000000..48c9e23f7be2 --- /dev/null +++ b/src/main/java/teammates/storage/sqlapi/FeedbackSessionLogsDb.java @@ -0,0 +1,86 @@ +package teammates.storage.sqlapi; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import teammates.common.util.HibernateUtil; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.storage.sqlentity.Student; + +/** + * Handles CRUD operations for feedback session logs. + * + * @see FeedbackSessionLog + */ +public final class FeedbackSessionLogsDb extends EntitiesDb { + + private static final FeedbackSessionLogsDb instance = new FeedbackSessionLogsDb(); + + private FeedbackSessionLogsDb() { + // prevent initialization + } + + public static FeedbackSessionLogsDb inst() { + return instance; + } + + /** + * Gets the feedback session logs as filtered by the given parameters ordered by + * ascending timestamp. Logs with the same timestamp will be ordered by the + * student's email. + * + * @param studentId Can be null + * @param feedbackSessionId Can be null + */ + public List getOrderedFeedbackSessionLogs(String courseId, UUID studentId, + UUID feedbackSessionId, Instant startTime, Instant endTime) { + + assert courseId != null; + assert startTime != null; + assert endTime != null; + + CriteriaBuilder cb = HibernateUtil.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery(FeedbackSessionLog.class); + Root root = cr.from(FeedbackSessionLog.class); + Join feedbackSessionJoin = root.join("feedbackSession"); + Join studentJoin = root.join("student"); + + List predicates = new ArrayList<>(); + + if (studentId != null) { + predicates.add(cb.equal(studentJoin.get("id"), studentId)); + } + + if (feedbackSessionId != null) { + predicates.add(cb.equal(feedbackSessionJoin.get("id"), feedbackSessionId)); + } + + predicates.add(cb.equal(feedbackSessionJoin.get("course").get("id"), courseId)); + predicates.add(cb.greaterThanOrEqualTo(root.get("timestamp"), startTime)); + predicates.add(cb.lessThan(root.get("timestamp"), endTime)); + + cr.select(root).where(predicates.toArray(new Predicate[0])).orderBy(cb.asc(root.get("timestamp")), + cb.asc(studentJoin.get("email"))); + return HibernateUtil.createQuery(cr).getResultList(); + } + + /** + * Creates feedback session logs. + */ + public FeedbackSessionLog createFeedbackSessionLog(FeedbackSessionLog log) { + assert log != null; + + persist(log); + + return log; + } +} diff --git a/src/main/java/teammates/storage/sqlapi/FeedbackSessionsDb.java b/src/main/java/teammates/storage/sqlapi/FeedbackSessionsDb.java index 389407e9f28a..3303551e763e 100644 --- a/src/main/java/teammates/storage/sqlapi/FeedbackSessionsDb.java +++ b/src/main/java/teammates/storage/sqlapi/FeedbackSessionsDb.java @@ -8,6 +8,11 @@ import java.util.UUID; import java.util.stream.Collectors; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Root; + import teammates.common.exception.EntityAlreadyExistsException; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; @@ -16,11 +21,6 @@ import teammates.storage.sqlentity.Course; import teammates.storage.sqlentity.FeedbackSession; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Join; -import jakarta.persistence.criteria.Root; - /** * Handles CRUD operations for feedback sessions. * @@ -65,6 +65,17 @@ public FeedbackSession getFeedbackSession(String feedbackSessionName, String cou return HibernateUtil.createQuery(cq).getResultStream().findFirst().orElse(null); } + /** + * Gets a feedback session reference. + * + * @return Returns a proxy for the feedback session. + */ + public FeedbackSession getFeedbackSessionReference(UUID id) { + assert id != null; + + return HibernateUtil.getReference(FeedbackSession.class, id); + } + /** * Gets a soft-deleted feedback session. * @@ -185,9 +196,9 @@ public void deleteFeedbackSession(FeedbackSession feedbackSession) { /** * Soft-deletes a specific feedback session by its name and course id. * - * @return Soft-deletion time of the feedback session. + * @return the feedback session. */ - public Instant softDeleteFeedbackSession(String feedbackSessionName, String courseId) + public FeedbackSession softDeleteFeedbackSession(String feedbackSessionName, String courseId) throws EntityDoesNotExistException { assert courseId != null; assert feedbackSessionName != null; @@ -201,7 +212,7 @@ public Instant softDeleteFeedbackSession(String feedbackSessionName, String cour feedbackSessionEntity.setDeletedAt(Instant.now()); merge(feedbackSessionEntity); - return feedbackSessionEntity.getDeletedAt(); + return feedbackSessionEntity; } /** diff --git a/src/main/java/teammates/storage/sqlapi/NotificationsDb.java b/src/main/java/teammates/storage/sqlapi/NotificationsDb.java index 85e00e48b47f..3a3c114e42f1 100644 --- a/src/main/java/teammates/storage/sqlapi/NotificationsDb.java +++ b/src/main/java/teammates/storage/sqlapi/NotificationsDb.java @@ -4,17 +4,17 @@ import java.util.List; import java.util.UUID; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + import teammates.common.datatransfer.NotificationTargetUser; import teammates.common.exception.EntityAlreadyExistsException; import teammates.common.exception.InvalidParametersException; import teammates.common.util.HibernateUtil; import teammates.storage.sqlentity.Notification; -import jakarta.persistence.TypedQuery; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Root; - /** * Handles CRUD operations for notifications. * diff --git a/src/main/java/teammates/storage/sqlapi/UsageStatisticsDb.java b/src/main/java/teammates/storage/sqlapi/UsageStatisticsDb.java index e989975ea0c8..88c9be32bc3b 100644 --- a/src/main/java/teammates/storage/sqlapi/UsageStatisticsDb.java +++ b/src/main/java/teammates/storage/sqlapi/UsageStatisticsDb.java @@ -3,13 +3,13 @@ import java.time.Instant; import java.util.List; -import teammates.common.util.HibernateUtil; -import teammates.storage.sqlentity.UsageStatistics; - import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; +import teammates.common.util.HibernateUtil; +import teammates.storage.sqlentity.UsageStatistics; + /** * Handles CRUD operations for usage statistics. * diff --git a/src/main/java/teammates/storage/sqlapi/UsersDb.java b/src/main/java/teammates/storage/sqlapi/UsersDb.java index 5d3b8571071c..56a370cf05a9 100644 --- a/src/main/java/teammates/storage/sqlapi/UsersDb.java +++ b/src/main/java/teammates/storage/sqlapi/UsersDb.java @@ -7,6 +7,12 @@ import java.util.List; import java.util.UUID; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + import teammates.common.exception.EntityAlreadyExistsException; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; @@ -23,12 +29,6 @@ import teammates.storage.sqlsearch.SearchManagerFactory; import teammates.storage.sqlsearch.StudentSearchManager; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Join; -import jakarta.persistence.criteria.Predicate; -import jakarta.persistence.criteria.Root; - /** * Handles CRUD operations for users. * @@ -146,6 +146,15 @@ public Student getStudent(UUID id) { return HibernateUtil.get(Student.class, id); } + /** + * Gets a student reference by its {@code id}. + */ + public Student getStudentReference(UUID id) { + assert id != null; + + return HibernateUtil.getReference(Student.class, id); + } + /** * Gets a student by {@code regKey}. */ diff --git a/src/main/java/teammates/storage/sqlentity/Account.java b/src/main/java/teammates/storage/sqlentity/Account.java index 3a5ea49d7f1b..a02aa77ecc97 100644 --- a/src/main/java/teammates/storage/sqlentity/Account.java +++ b/src/main/java/teammates/storage/sqlentity/Account.java @@ -6,6 +6,13 @@ import java.util.Objects; import java.util.UUID; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + import org.hibernate.annotations.NaturalId; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; @@ -14,13 +21,6 @@ import teammates.common.util.FieldValidator; import teammates.common.util.SanitizationHelper; -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Table; - /** * Represents a unique account in the system. */ diff --git a/src/main/java/teammates/storage/sqlentity/AccountRequest.java b/src/main/java/teammates/storage/sqlentity/AccountRequest.java index 2389fbf352df..70937c8243cb 100644 --- a/src/main/java/teammates/storage/sqlentity/AccountRequest.java +++ b/src/main/java/teammates/storage/sqlentity/AccountRequest.java @@ -7,19 +7,23 @@ import java.util.Objects; import java.util.UUID; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; + import org.hibernate.annotations.UpdateTimestamp; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.util.Config; import teammates.common.util.Const; import teammates.common.util.FieldValidator; import teammates.common.util.SanitizationHelper; import teammates.common.util.StringHelper; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; - /** * Entity for AccountRequests. */ @@ -27,7 +31,6 @@ @Table(name = "AccountRequests", uniqueConstraints = { @UniqueConstraint(name = "Unique registration key", columnNames = "registrationKey"), - @UniqueConstraint(name = "Unique name and institute", columnNames = {"email", "institute"}) }) public class AccountRequest extends BaseEntity { @Id @@ -41,6 +44,12 @@ public class AccountRequest extends BaseEntity { private String institute; + @Enumerated(EnumType.STRING) + private AccountRequestStatus status; + + @Column(columnDefinition = "TEXT") + private String comments; + private Instant registeredAt; @UpdateTimestamp @@ -50,11 +59,13 @@ protected AccountRequest() { // required by Hibernate } - public AccountRequest(String email, String name, String institute) { + public AccountRequest(String email, String name, String institute, AccountRequestStatus status, String comments) { this.setId(UUID.randomUUID()); this.setEmail(email); this.setName(name); this.setInstitute(institute); + this.setStatus(status); + this.setComments(comments); this.generateNewRegistrationKey(); this.setCreatedAt(Instant.now()); this.setRegisteredAt(null); @@ -129,6 +140,22 @@ public void setInstitute(String institute) { this.institute = SanitizationHelper.sanitizeTitle(institute); } + public AccountRequestStatus getStatus() { + return this.status; + } + + public void setStatus(AccountRequestStatus status) { + this.status = status; + } + + public String getComments() { + return this.comments; + } + + public void setComments(String comments) { + this.comments = comments; + } + public Instant getRegisteredAt() { return this.registeredAt; } @@ -167,8 +194,8 @@ public int hashCode() { @Override public String toString() { return "AccountRequest [id=" + id + ", registrationKey=" + registrationKey + ", name=" + name + ", email=" - + email + ", institute=" + institute + ", registeredAt=" + registeredAt + ", createdAt=" + getCreatedAt() - + ", updatedAt=" + updatedAt + "]"; + + email + ", institute=" + institute + ", status=" + status + ", comments=" + comments + + ", registeredAt=" + registeredAt + ", createdAt=" + getCreatedAt() + ", updatedAt=" + updatedAt + "]"; } public String getRegistrationUrl() { diff --git a/src/main/java/teammates/storage/sqlentity/BaseEntity.java b/src/main/java/teammates/storage/sqlentity/BaseEntity.java index b52fe20ca703..440a09c20709 100644 --- a/src/main/java/teammates/storage/sqlentity/BaseEntity.java +++ b/src/main/java/teammates/storage/sqlentity/BaseEntity.java @@ -4,6 +4,12 @@ import java.time.Instant; import java.util.List; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Column; +import jakarta.persistence.Converter; +import jakarta.persistence.MappedSuperclass; + + import com.google.common.reflect.TypeToken; import teammates.common.datatransfer.FeedbackParticipantType; @@ -13,11 +19,6 @@ import teammates.common.datatransfer.questions.FeedbackResponseDetails; import teammates.common.util.JsonUtils; -import jakarta.persistence.AttributeConverter; -import jakarta.persistence.Column; -import jakarta.persistence.Converter; -import jakarta.persistence.MappedSuperclass; - /** * Base class for all entities. */ diff --git a/src/main/java/teammates/storage/sqlentity/Course.java b/src/main/java/teammates/storage/sqlentity/Course.java index 6e8515063000..25c283b6525f 100644 --- a/src/main/java/teammates/storage/sqlentity/Course.java +++ b/src/main/java/teammates/storage/sqlentity/Course.java @@ -5,13 +5,6 @@ import java.util.List; import java.util.Objects; -import org.apache.commons.lang.StringUtils; -import org.hibernate.annotations.UpdateTimestamp; - -import teammates.common.util.Const; -import teammates.common.util.FieldValidator; -import teammates.common.util.SanitizationHelper; - import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -19,6 +12,13 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import org.apache.commons.lang.StringUtils; +import org.hibernate.annotations.UpdateTimestamp; + +import teammates.common.util.Const; +import teammates.common.util.FieldValidator; +import teammates.common.util.SanitizationHelper; + /** * Represents a course. */ diff --git a/src/main/java/teammates/storage/sqlentity/DeadlineExtension.java b/src/main/java/teammates/storage/sqlentity/DeadlineExtension.java index ba7ecb1afa52..d8814be86910 100644 --- a/src/main/java/teammates/storage/sqlentity/DeadlineExtension.java +++ b/src/main/java/teammates/storage/sqlentity/DeadlineExtension.java @@ -6,11 +6,6 @@ import java.util.Objects; import java.util.UUID; -import org.hibernate.annotations.OnDelete; -import org.hibernate.annotations.OnDeleteAction; - -import teammates.common.util.FieldValidator; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; @@ -18,6 +13,12 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.annotations.UpdateTimestamp; + +import teammates.common.util.FieldValidator; + /** * Represents a deadline extension entity. */ diff --git a/src/main/java/teammates/storage/sqlentity/FeedbackQuestion.java b/src/main/java/teammates/storage/sqlentity/FeedbackQuestion.java index 872b61516082..537b8d70ae92 100644 --- a/src/main/java/teammates/storage/sqlentity/FeedbackQuestion.java +++ b/src/main/java/teammates/storage/sqlentity/FeedbackQuestion.java @@ -6,19 +6,6 @@ import java.util.Objects; import java.util.UUID; -import teammates.common.datatransfer.FeedbackParticipantType; -import teammates.common.datatransfer.questions.FeedbackQuestionDetails; -import teammates.common.util.FieldValidator; -import teammates.storage.sqlentity.questions.FeedbackConstantSumQuestion; -import teammates.storage.sqlentity.questions.FeedbackContributionQuestion; -import teammates.storage.sqlentity.questions.FeedbackMcqQuestion; -import teammates.storage.sqlentity.questions.FeedbackMsqQuestion; -import teammates.storage.sqlentity.questions.FeedbackNumericalScaleQuestion; -import teammates.storage.sqlentity.questions.FeedbackRankOptionsQuestion; -import teammates.storage.sqlentity.questions.FeedbackRankRecipientsQuestion; -import teammates.storage.sqlentity.questions.FeedbackRubricQuestion; -import teammates.storage.sqlentity.questions.FeedbackTextQuestion; - import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Convert; @@ -33,6 +20,21 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import org.hibernate.annotations.UpdateTimestamp; + +import teammates.common.datatransfer.FeedbackParticipantType; +import teammates.common.datatransfer.questions.FeedbackQuestionDetails; +import teammates.common.util.FieldValidator; +import teammates.storage.sqlentity.questions.FeedbackConstantSumQuestion; +import teammates.storage.sqlentity.questions.FeedbackContributionQuestion; +import teammates.storage.sqlentity.questions.FeedbackMcqQuestion; +import teammates.storage.sqlentity.questions.FeedbackMsqQuestion; +import teammates.storage.sqlentity.questions.FeedbackNumericalScaleQuestion; +import teammates.storage.sqlentity.questions.FeedbackRankOptionsQuestion; +import teammates.storage.sqlentity.questions.FeedbackRankRecipientsQuestion; +import teammates.storage.sqlentity.questions.FeedbackRubricQuestion; +import teammates.storage.sqlentity.questions.FeedbackTextQuestion; + /** * Represents a feedback question. */ diff --git a/src/main/java/teammates/storage/sqlentity/FeedbackResponse.java b/src/main/java/teammates/storage/sqlentity/FeedbackResponse.java index d4ce1a5db9ea..25a69df23cd4 100644 --- a/src/main/java/teammates/storage/sqlentity/FeedbackResponse.java +++ b/src/main/java/teammates/storage/sqlentity/FeedbackResponse.java @@ -6,17 +6,6 @@ import java.util.Objects; import java.util.UUID; -import teammates.common.datatransfer.questions.FeedbackResponseDetails; -import teammates.storage.sqlentity.responses.FeedbackConstantSumResponse; -import teammates.storage.sqlentity.responses.FeedbackContributionResponse; -import teammates.storage.sqlentity.responses.FeedbackMcqResponse; -import teammates.storage.sqlentity.responses.FeedbackMsqResponse; -import teammates.storage.sqlentity.responses.FeedbackNumericalScaleResponse; -import teammates.storage.sqlentity.responses.FeedbackRankOptionsResponse; -import teammates.storage.sqlentity.responses.FeedbackRankRecipientsResponse; -import teammates.storage.sqlentity.responses.FeedbackRubricResponse; -import teammates.storage.sqlentity.responses.FeedbackTextResponse; - import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -28,6 +17,19 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import org.hibernate.annotations.UpdateTimestamp; + +import teammates.common.datatransfer.questions.FeedbackResponseDetails; +import teammates.storage.sqlentity.responses.FeedbackConstantSumResponse; +import teammates.storage.sqlentity.responses.FeedbackContributionResponse; +import teammates.storage.sqlentity.responses.FeedbackMcqResponse; +import teammates.storage.sqlentity.responses.FeedbackMsqResponse; +import teammates.storage.sqlentity.responses.FeedbackNumericalScaleResponse; +import teammates.storage.sqlentity.responses.FeedbackRankOptionsResponse; +import teammates.storage.sqlentity.responses.FeedbackRankRecipientsResponse; +import teammates.storage.sqlentity.responses.FeedbackRubricResponse; +import teammates.storage.sqlentity.responses.FeedbackTextResponse; + /** * Represents a Feedback Response. */ @@ -148,7 +150,7 @@ public static FeedbackResponse updateResponse( Section giverSection, String receiver, Section receiverSection, FeedbackResponseDetails responseDetails ) { - FeedbackResponse updatedFeedbackResponse = FeedbackResponse.makeResponse( + FeedbackResponse updatedFeedbackResponse = makeResponse( feedbackQuestion, giver, giverSection, diff --git a/src/main/java/teammates/storage/sqlentity/FeedbackResponseComment.java b/src/main/java/teammates/storage/sqlentity/FeedbackResponseComment.java index 5e0da2e828cf..41aae236fc8c 100644 --- a/src/main/java/teammates/storage/sqlentity/FeedbackResponseComment.java +++ b/src/main/java/teammates/storage/sqlentity/FeedbackResponseComment.java @@ -5,13 +5,6 @@ import java.util.List; import java.util.Objects; -import org.hibernate.annotations.OnDelete; -import org.hibernate.annotations.OnDeleteAction; - -import teammates.common.datatransfer.FeedbackParticipantType; -import teammates.common.util.FieldValidator; -import teammates.common.util.SanitizationHelper; - import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; @@ -21,6 +14,14 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.annotations.UpdateTimestamp; + +import teammates.common.datatransfer.FeedbackParticipantType; +import teammates.common.util.FieldValidator; +import teammates.common.util.SanitizationHelper; + /** * Represents a feedback response comment. */ diff --git a/src/main/java/teammates/storage/sqlentity/FeedbackSession.java b/src/main/java/teammates/storage/sqlentity/FeedbackSession.java index 6762acc74a61..195c3c732d10 100644 --- a/src/main/java/teammates/storage/sqlentity/FeedbackSession.java +++ b/src/main/java/teammates/storage/sqlentity/FeedbackSession.java @@ -7,16 +7,6 @@ import java.util.Objects; import java.util.UUID; -import org.apache.commons.lang.StringUtils; -import org.hibernate.annotations.Fetch; -import org.hibernate.annotations.FetchMode; -import org.hibernate.annotations.OnDelete; -import org.hibernate.annotations.OnDeleteAction; - -import teammates.common.util.Const; -import teammates.common.util.FieldValidator; -import teammates.common.util.SanitizationHelper; - import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Convert; @@ -28,6 +18,15 @@ import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; +import org.apache.commons.lang.StringUtils; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.annotations.UpdateTimestamp; + +import teammates.common.util.Const; +import teammates.common.util.FieldValidator; +import teammates.common.util.SanitizationHelper; + /** * Represents a course entity. */ @@ -91,12 +90,10 @@ public class FeedbackSession extends BaseEntity { private boolean isPublishedEmailSent; @OneToMany(mappedBy = "feedbackSession", cascade = CascadeType.REMOVE) - @Fetch(FetchMode.JOIN) @OnDelete(action = OnDeleteAction.CASCADE) private List deadlineExtensions = new ArrayList<>(); @OneToMany(mappedBy = "feedbackSession", cascade = CascadeType.REMOVE) - @Fetch(FetchMode.JOIN) @OnDelete(action = OnDeleteAction.CASCADE) private List feedbackQuestions = new ArrayList<>(); @@ -143,7 +140,12 @@ public FeedbackSession getCopyForUser(String userEmail) { return copy; } - private FeedbackSession getCopy() { + /** + * Creates a copy of the feedback session. + * + * @return The copy of this object. + */ + public FeedbackSession getCopy() { FeedbackSession fs = new FeedbackSession( name, course, creatorEmail, instructions, startTime, endTime, sessionVisibleFromTime, resultsVisibleFromTime, @@ -510,7 +512,7 @@ public boolean isInGracePeriodGivenExtendedDeadline(Instant extendedDeadline) { } /** - * Returns {@code true} if the results of the feedback session is visible; {@code false} if not. + * Returns {@code true} if the results of the feedback session is published; {@code false} if not. * Does not care if the session has ended or not. */ public boolean isPublished() { diff --git a/src/main/java/teammates/storage/sqlentity/FeedbackSessionLog.java b/src/main/java/teammates/storage/sqlentity/FeedbackSessionLog.java new file mode 100644 index 000000000000..7027f4182887 --- /dev/null +++ b/src/main/java/teammates/storage/sqlentity/FeedbackSessionLog.java @@ -0,0 +1,135 @@ +package teammates.storage.sqlentity; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import teammates.common.datatransfer.logs.FeedbackSessionLogType; + +/** + * Represents a feedback session log. + */ +@Entity +@Table(name = "FeedbackSessionLogs") +public class FeedbackSessionLog extends BaseEntity { + @Id + private UUID id; + + @ManyToOne + @JoinColumn(name = "studentId") + @NotFound(action = NotFoundAction.IGNORE) + @OnDelete(action = OnDeleteAction.CASCADE) + private Student student; + + @ManyToOne + @JoinColumn(name = "sessionId") + @NotFound(action = NotFoundAction.IGNORE) + @OnDelete(action = OnDeleteAction.CASCADE) + private FeedbackSession feedbackSession; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private FeedbackSessionLogType feedbackSessionLogType; + + @Column(nullable = false) + private Instant timestamp; + + protected FeedbackSessionLog() { + // required by Hibernate + } + + public FeedbackSessionLog(Student student, FeedbackSession feedbackSession, + FeedbackSessionLogType feedbackSessionLogType, Instant timestamp) { + this.setId(UUID.randomUUID()); + this.student = student; + this.feedbackSession = feedbackSession; + this.feedbackSessionLogType = feedbackSessionLogType; + this.timestamp = timestamp; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public Student getStudent() { + return student; + } + + public void setStudent(Student student) { + this.student = student; + } + + public FeedbackSession getFeedbackSession() { + return feedbackSession; + } + + public void setFeedbackSession(FeedbackSession feedbackSession) { + this.feedbackSession = feedbackSession; + } + + public FeedbackSessionLogType getFeedbackSessionLogType() { + return feedbackSessionLogType; + } + + public void setFeedbackSessionLogType(FeedbackSessionLogType feedbackSessionLogType) { + this.feedbackSessionLogType = feedbackSessionLogType; + } + + public Instant getTimestamp() { + return timestamp; + } + + public void setTimestamp(Instant timestamp) { + this.timestamp = timestamp; + } + + @Override + public String toString() { + return "FeedbackSessionLog [id=" + id + ", student=" + student + ", feedbackSession=" + feedbackSession + + ", feedbackSessionLogType=" + feedbackSessionLogType.getLabel() + ", timestamp=" + timestamp + "]"; + } + + @Override + public int hashCode() { + return this.getId().hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } else if (this == other) { + return true; + } else if (this.getClass() == other.getClass()) { + FeedbackSessionLog otherFeedbackSessionLog = (FeedbackSessionLog) other; + return Objects.equals(this.getId(), otherFeedbackSessionLog.getId()); + } else { + return false; + } + } + + @Override + public List getInvalidityInfo() { + return new ArrayList<>(); + } +} diff --git a/src/main/java/teammates/storage/sqlentity/Instructor.java b/src/main/java/teammates/storage/sqlentity/Instructor.java index 6848d80ac4b1..418d946a6039 100644 --- a/src/main/java/teammates/storage/sqlentity/Instructor.java +++ b/src/main/java/teammates/storage/sqlentity/Instructor.java @@ -4,6 +4,13 @@ import java.util.List; import java.util.Map; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; + import teammates.common.datatransfer.InstructorPermissionRole; import teammates.common.datatransfer.InstructorPermissionSet; import teammates.common.datatransfer.InstructorPrivileges; @@ -12,13 +19,6 @@ import teammates.common.util.FieldValidator; import teammates.common.util.SanitizationHelper; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.Table; - /** * Represents an Instructor. */ diff --git a/src/main/java/teammates/storage/sqlentity/Notification.java b/src/main/java/teammates/storage/sqlentity/Notification.java index 3707089bb60a..f167af929fc2 100644 --- a/src/main/java/teammates/storage/sqlentity/Notification.java +++ b/src/main/java/teammates/storage/sqlentity/Notification.java @@ -6,15 +6,6 @@ import java.util.Objects; import java.util.UUID; -import org.hibernate.annotations.OnDelete; -import org.hibernate.annotations.OnDeleteAction; -import org.hibernate.annotations.UpdateTimestamp; - -import teammates.common.datatransfer.NotificationStyle; -import teammates.common.datatransfer.NotificationTargetUser; -import teammates.common.util.FieldValidator; -import teammates.common.util.SanitizationHelper; - import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -24,6 +15,15 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.annotations.UpdateTimestamp; + +import teammates.common.datatransfer.NotificationStyle; +import teammates.common.datatransfer.NotificationTargetUser; +import teammates.common.util.FieldValidator; +import teammates.common.util.SanitizationHelper; + /** * Represents a unique notification in the system. */ diff --git a/src/main/java/teammates/storage/sqlentity/Section.java b/src/main/java/teammates/storage/sqlentity/Section.java index 7dba45edd8ee..b1a5f076540e 100644 --- a/src/main/java/teammates/storage/sqlentity/Section.java +++ b/src/main/java/teammates/storage/sqlentity/Section.java @@ -6,13 +6,6 @@ import java.util.Objects; import java.util.UUID; -import org.hibernate.annotations.OnDelete; -import org.hibernate.annotations.OnDeleteAction; -import org.hibernate.annotations.UpdateTimestamp; - -import teammates.common.util.FieldValidator; -import teammates.common.util.SanitizationHelper; - import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -22,6 +15,13 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.annotations.UpdateTimestamp; + +import teammates.common.util.FieldValidator; +import teammates.common.util.SanitizationHelper; + /** * Represents a Section. */ diff --git a/src/main/java/teammates/storage/sqlentity/Student.java b/src/main/java/teammates/storage/sqlentity/Student.java index 3c4601dbe071..0a2cf3273ce4 100644 --- a/src/main/java/teammates/storage/sqlentity/Student.java +++ b/src/main/java/teammates/storage/sqlentity/Student.java @@ -3,15 +3,15 @@ import java.util.ArrayList; import java.util.List; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + import teammates.common.util.Config; import teammates.common.util.Const; import teammates.common.util.FieldValidator; import teammates.common.util.SanitizationHelper; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; - /** * Represents a Student. */ diff --git a/src/main/java/teammates/storage/sqlentity/Team.java b/src/main/java/teammates/storage/sqlentity/Team.java index 587d5ceefe65..bff9d7dfb160 100644 --- a/src/main/java/teammates/storage/sqlentity/Team.java +++ b/src/main/java/teammates/storage/sqlentity/Team.java @@ -6,10 +6,6 @@ import java.util.Objects; import java.util.UUID; -import org.hibernate.annotations.UpdateTimestamp; - -import teammates.common.util.FieldValidator; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; @@ -18,6 +14,10 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import org.hibernate.annotations.UpdateTimestamp; + +import teammates.common.util.FieldValidator; + /** * Represents a Team. */ diff --git a/src/main/java/teammates/storage/sqlentity/User.java b/src/main/java/teammates/storage/sqlentity/User.java index 7b5b8ed1bc0b..c43a3427bc43 100644 --- a/src/main/java/teammates/storage/sqlentity/User.java +++ b/src/main/java/teammates/storage/sqlentity/User.java @@ -5,9 +5,6 @@ import java.util.Objects; import java.util.UUID; -import teammates.common.util.SanitizationHelper; -import teammates.common.util.StringHelper; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; @@ -18,6 +15,11 @@ import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; +import org.hibernate.annotations.UpdateTimestamp; + +import teammates.common.util.SanitizationHelper; +import teammates.common.util.StringHelper; + /** * Represents a User. */ diff --git a/src/main/java/teammates/storage/sqlentity/questions/FeedbackConstantSumQuestion.java b/src/main/java/teammates/storage/sqlentity/questions/FeedbackConstantSumQuestion.java index 8b5b559c1d35..add060fd134e 100644 --- a/src/main/java/teammates/storage/sqlentity/questions/FeedbackConstantSumQuestion.java +++ b/src/main/java/teammates/storage/sqlentity/questions/FeedbackConstantSumQuestion.java @@ -3,17 +3,17 @@ import java.util.ArrayList; import java.util.List; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.questions.FeedbackConstantSumQuestionDetails; import teammates.common.datatransfer.questions.FeedbackQuestionDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackSession; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a constant sum question. */ diff --git a/src/main/java/teammates/storage/sqlentity/questions/FeedbackContributionQuestion.java b/src/main/java/teammates/storage/sqlentity/questions/FeedbackContributionQuestion.java index dbd6474c3c08..fea203cc4430 100644 --- a/src/main/java/teammates/storage/sqlentity/questions/FeedbackContributionQuestion.java +++ b/src/main/java/teammates/storage/sqlentity/questions/FeedbackContributionQuestion.java @@ -3,17 +3,17 @@ import java.util.ArrayList; import java.util.List; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.questions.FeedbackContributionQuestionDetails; import teammates.common.datatransfer.questions.FeedbackQuestionDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackSession; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a contribution question. */ diff --git a/src/main/java/teammates/storage/sqlentity/questions/FeedbackMcqQuestion.java b/src/main/java/teammates/storage/sqlentity/questions/FeedbackMcqQuestion.java index 3cf3821776b1..593d7c521c0f 100644 --- a/src/main/java/teammates/storage/sqlentity/questions/FeedbackMcqQuestion.java +++ b/src/main/java/teammates/storage/sqlentity/questions/FeedbackMcqQuestion.java @@ -3,17 +3,17 @@ import java.util.ArrayList; import java.util.List; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.questions.FeedbackMcqQuestionDetails; import teammates.common.datatransfer.questions.FeedbackQuestionDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackSession; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents an mcq question. */ diff --git a/src/main/java/teammates/storage/sqlentity/questions/FeedbackMsqQuestion.java b/src/main/java/teammates/storage/sqlentity/questions/FeedbackMsqQuestion.java index 329e194474e4..18d83168a894 100644 --- a/src/main/java/teammates/storage/sqlentity/questions/FeedbackMsqQuestion.java +++ b/src/main/java/teammates/storage/sqlentity/questions/FeedbackMsqQuestion.java @@ -3,17 +3,17 @@ import java.util.ArrayList; import java.util.List; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.questions.FeedbackMsqQuestionDetails; import teammates.common.datatransfer.questions.FeedbackQuestionDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackSession; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents an msq question. */ diff --git a/src/main/java/teammates/storage/sqlentity/questions/FeedbackNumericalScaleQuestion.java b/src/main/java/teammates/storage/sqlentity/questions/FeedbackNumericalScaleQuestion.java index 7da16e3eee06..5f5c629d8f63 100644 --- a/src/main/java/teammates/storage/sqlentity/questions/FeedbackNumericalScaleQuestion.java +++ b/src/main/java/teammates/storage/sqlentity/questions/FeedbackNumericalScaleQuestion.java @@ -3,17 +3,17 @@ import java.util.ArrayList; import java.util.List; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.questions.FeedbackNumericalScaleQuestionDetails; import teammates.common.datatransfer.questions.FeedbackQuestionDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackSession; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a numerical scale question. */ diff --git a/src/main/java/teammates/storage/sqlentity/questions/FeedbackRankOptionsQuestion.java b/src/main/java/teammates/storage/sqlentity/questions/FeedbackRankOptionsQuestion.java index ba135c7cae84..1c40fe12cb7d 100644 --- a/src/main/java/teammates/storage/sqlentity/questions/FeedbackRankOptionsQuestion.java +++ b/src/main/java/teammates/storage/sqlentity/questions/FeedbackRankOptionsQuestion.java @@ -3,17 +3,17 @@ import java.util.ArrayList; import java.util.List; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.questions.FeedbackQuestionDetails; import teammates.common.datatransfer.questions.FeedbackRankOptionsQuestionDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackSession; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a rank options question. */ diff --git a/src/main/java/teammates/storage/sqlentity/questions/FeedbackRankRecipientsQuestion.java b/src/main/java/teammates/storage/sqlentity/questions/FeedbackRankRecipientsQuestion.java index 4f33ed813c71..7ade04fd0483 100644 --- a/src/main/java/teammates/storage/sqlentity/questions/FeedbackRankRecipientsQuestion.java +++ b/src/main/java/teammates/storage/sqlentity/questions/FeedbackRankRecipientsQuestion.java @@ -3,17 +3,17 @@ import java.util.ArrayList; import java.util.List; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.questions.FeedbackQuestionDetails; import teammates.common.datatransfer.questions.FeedbackRankRecipientsQuestionDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackSession; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a rank recipients question. */ diff --git a/src/main/java/teammates/storage/sqlentity/questions/FeedbackRubricQuestion.java b/src/main/java/teammates/storage/sqlentity/questions/FeedbackRubricQuestion.java index cbd3b1d4813d..4bf2911a7af3 100644 --- a/src/main/java/teammates/storage/sqlentity/questions/FeedbackRubricQuestion.java +++ b/src/main/java/teammates/storage/sqlentity/questions/FeedbackRubricQuestion.java @@ -3,17 +3,17 @@ import java.util.ArrayList; import java.util.List; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.questions.FeedbackQuestionDetails; import teammates.common.datatransfer.questions.FeedbackRubricQuestionDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackSession; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a rubric question. */ diff --git a/src/main/java/teammates/storage/sqlentity/questions/FeedbackTextQuestion.java b/src/main/java/teammates/storage/sqlentity/questions/FeedbackTextQuestion.java index 10ee7ca42ebb..6e71281a1ab4 100644 --- a/src/main/java/teammates/storage/sqlentity/questions/FeedbackTextQuestion.java +++ b/src/main/java/teammates/storage/sqlentity/questions/FeedbackTextQuestion.java @@ -3,17 +3,17 @@ import java.util.ArrayList; import java.util.List; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.questions.FeedbackQuestionDetails; import teammates.common.datatransfer.questions.FeedbackTextQuestionDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackSession; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a text question. */ diff --git a/src/main/java/teammates/storage/sqlentity/responses/FeedbackConstantSumResponse.java b/src/main/java/teammates/storage/sqlentity/responses/FeedbackConstantSumResponse.java index 83a54c4c1959..72f5736fa4d3 100644 --- a/src/main/java/teammates/storage/sqlentity/responses/FeedbackConstantSumResponse.java +++ b/src/main/java/teammates/storage/sqlentity/responses/FeedbackConstantSumResponse.java @@ -1,16 +1,16 @@ package teammates.storage.sqlentity.responses; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.questions.FeedbackConstantSumResponseDetails; import teammates.common.datatransfer.questions.FeedbackResponseDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.Section; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a feedback constant sum response. */ diff --git a/src/main/java/teammates/storage/sqlentity/responses/FeedbackContributionResponse.java b/src/main/java/teammates/storage/sqlentity/responses/FeedbackContributionResponse.java index 52b0dcef2f00..0c92a7649409 100644 --- a/src/main/java/teammates/storage/sqlentity/responses/FeedbackContributionResponse.java +++ b/src/main/java/teammates/storage/sqlentity/responses/FeedbackContributionResponse.java @@ -1,16 +1,16 @@ package teammates.storage.sqlentity.responses; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.questions.FeedbackContributionResponseDetails; import teammates.common.datatransfer.questions.FeedbackResponseDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.Section; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a feedback contribution response. */ diff --git a/src/main/java/teammates/storage/sqlentity/responses/FeedbackMcqResponse.java b/src/main/java/teammates/storage/sqlentity/responses/FeedbackMcqResponse.java index 151dfe0e7338..a30a4fde99e9 100644 --- a/src/main/java/teammates/storage/sqlentity/responses/FeedbackMcqResponse.java +++ b/src/main/java/teammates/storage/sqlentity/responses/FeedbackMcqResponse.java @@ -1,16 +1,16 @@ package teammates.storage.sqlentity.responses; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.questions.FeedbackMcqResponseDetails; import teammates.common.datatransfer.questions.FeedbackResponseDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.Section; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a feedback mcq response. */ diff --git a/src/main/java/teammates/storage/sqlentity/responses/FeedbackMsqResponse.java b/src/main/java/teammates/storage/sqlentity/responses/FeedbackMsqResponse.java index 4f2aa905f5cf..a88f7fcd89e1 100644 --- a/src/main/java/teammates/storage/sqlentity/responses/FeedbackMsqResponse.java +++ b/src/main/java/teammates/storage/sqlentity/responses/FeedbackMsqResponse.java @@ -1,16 +1,16 @@ package teammates.storage.sqlentity.responses; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.questions.FeedbackMsqResponseDetails; import teammates.common.datatransfer.questions.FeedbackResponseDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.Section; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a feedback msq response. */ diff --git a/src/main/java/teammates/storage/sqlentity/responses/FeedbackNumericalScaleResponse.java b/src/main/java/teammates/storage/sqlentity/responses/FeedbackNumericalScaleResponse.java index a951d2853562..a536dcc6195e 100644 --- a/src/main/java/teammates/storage/sqlentity/responses/FeedbackNumericalScaleResponse.java +++ b/src/main/java/teammates/storage/sqlentity/responses/FeedbackNumericalScaleResponse.java @@ -1,16 +1,16 @@ package teammates.storage.sqlentity.responses; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.questions.FeedbackNumericalScaleResponseDetails; import teammates.common.datatransfer.questions.FeedbackResponseDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.Section; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a feedback numerical scale response. */ diff --git a/src/main/java/teammates/storage/sqlentity/responses/FeedbackRankOptionsResponse.java b/src/main/java/teammates/storage/sqlentity/responses/FeedbackRankOptionsResponse.java index 00c542bee26c..c02fa900a6fb 100644 --- a/src/main/java/teammates/storage/sqlentity/responses/FeedbackRankOptionsResponse.java +++ b/src/main/java/teammates/storage/sqlentity/responses/FeedbackRankOptionsResponse.java @@ -1,16 +1,16 @@ package teammates.storage.sqlentity.responses; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.questions.FeedbackRankOptionsResponseDetails; import teammates.common.datatransfer.questions.FeedbackResponseDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.Section; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a feedback rank options response. */ diff --git a/src/main/java/teammates/storage/sqlentity/responses/FeedbackRankRecipientsResponse.java b/src/main/java/teammates/storage/sqlentity/responses/FeedbackRankRecipientsResponse.java index 9961fb7de11e..908b3a5734c3 100644 --- a/src/main/java/teammates/storage/sqlentity/responses/FeedbackRankRecipientsResponse.java +++ b/src/main/java/teammates/storage/sqlentity/responses/FeedbackRankRecipientsResponse.java @@ -1,16 +1,16 @@ package teammates.storage.sqlentity.responses; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.questions.FeedbackRankRecipientsResponseDetails; import teammates.common.datatransfer.questions.FeedbackResponseDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.Section; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a feedback rank recipients response. */ diff --git a/src/main/java/teammates/storage/sqlentity/responses/FeedbackRubricResponse.java b/src/main/java/teammates/storage/sqlentity/responses/FeedbackRubricResponse.java index 46997775a9b1..e8a984aa5b85 100644 --- a/src/main/java/teammates/storage/sqlentity/responses/FeedbackRubricResponse.java +++ b/src/main/java/teammates/storage/sqlentity/responses/FeedbackRubricResponse.java @@ -1,16 +1,16 @@ package teammates.storage.sqlentity.responses; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.questions.FeedbackResponseDetails; import teammates.common.datatransfer.questions.FeedbackRubricResponseDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.Section; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a feedback rubric response. */ diff --git a/src/main/java/teammates/storage/sqlentity/responses/FeedbackTextResponse.java b/src/main/java/teammates/storage/sqlentity/responses/FeedbackTextResponse.java index a56f6a58ab1a..bc7f583a33f2 100644 --- a/src/main/java/teammates/storage/sqlentity/responses/FeedbackTextResponse.java +++ b/src/main/java/teammates/storage/sqlentity/responses/FeedbackTextResponse.java @@ -1,16 +1,16 @@ package teammates.storage.sqlentity.responses; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; + import teammates.common.datatransfer.questions.FeedbackResponseDetails; import teammates.common.datatransfer.questions.FeedbackTextResponseDetails; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.Section; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Converter; -import jakarta.persistence.Entity; - /** * Represents a text response. */ diff --git a/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchDocument.java b/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchDocument.java index 9fbaf38ef149..15b6dd02e375 100644 --- a/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchDocument.java +++ b/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchDocument.java @@ -1,5 +1,6 @@ package teammates.storage.sqlsearch; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -19,14 +20,29 @@ class AccountRequestSearchDocument extends SearchDocument { Map getSearchableFields() { Map fields = new HashMap<>(); AccountRequest accountRequest = entity; - String[] searchableTexts = { - accountRequest.getName(), accountRequest.getEmail(), accountRequest.getInstitute(), - }; - fields.put("id", accountRequest.getId()); + ArrayList searchableTexts = new ArrayList<>(); + searchableTexts.add(accountRequest.getName()); + searchableTexts.add(accountRequest.getEmail()); + searchableTexts.add(accountRequest.getInstitute()); + + if (accountRequest.getComments() != null) { + searchableTexts.add(accountRequest.getComments()); + } + if (accountRequest.getStatus() != null) { + searchableTexts.add(accountRequest.getStatus().toString()); + } + + fields.put("id", accountRequest.getId().toString()); fields.put("_text_", String.join(" ", searchableTexts)); fields.put("email", accountRequest.getEmail()); fields.put("institute", accountRequest.getInstitute()); + if (accountRequest.getComments() != null) { + fields.put("comments", accountRequest.getComments()); + } + if (accountRequest.getStatus() != null) { + fields.put("status", accountRequest.getStatus().toString()); + } return fields; } diff --git a/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchManager.java b/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchManager.java index c5dc5d444289..5325836af322 100644 --- a/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchManager.java +++ b/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchManager.java @@ -2,6 +2,7 @@ import java.util.Comparator; import java.util.List; +import java.util.UUID; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.response.QueryResponse; @@ -35,9 +36,8 @@ AccountRequestSearchDocument createDocument(AccountRequest accountRequest) { @Override AccountRequest getEntityFromDocument(SolrDocument document) { - String email = (String) document.getFirstValue("email"); - String institute = (String) document.getFirstValue("institute"); - return accountRequestsDb.getAccountRequest(email, institute); + UUID id = UUID.fromString((String) document.getFieldValue("id")); + return accountRequestsDb.getAccountRequest(id); } @Override diff --git a/src/main/java/teammates/storage/sqlsearch/SearchManagerStarter.java b/src/main/java/teammates/storage/sqlsearch/SearchManagerStarter.java index 3775955a2b55..ce1956392996 100644 --- a/src/main/java/teammates/storage/sqlsearch/SearchManagerStarter.java +++ b/src/main/java/teammates/storage/sqlsearch/SearchManagerStarter.java @@ -1,7 +1,7 @@ package teammates.storage.sqlsearch; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; import teammates.common.util.Config; diff --git a/src/main/java/teammates/ui/constants/ApiConst.java b/src/main/java/teammates/ui/constants/ApiConst.java index ce946c8f3e6c..2a411e7b1bdf 100644 --- a/src/main/java/teammates/ui/constants/ApiConst.java +++ b/src/main/java/teammates/ui/constants/ApiConst.java @@ -28,7 +28,9 @@ public enum ApiConst { RANK_RECIPIENTS_ANSWER_NOT_SUBMITTED(Const.POINTS_NOT_SUBMITTED), NO_VALUE(Const.POINTS_NO_VALUE), LOGS_RETENTION_PERIOD(Const.LOGS_RETENTION_PERIOD.toDays()), - SEARCH_QUERY_SIZE_LIMIT(Const.SEARCH_QUERY_SIZE_LIMIT); + SEARCH_QUERY_SIZE_LIMIT(Const.SEARCH_QUERY_SIZE_LIMIT), + STUDENT_ACTIVITY_LOGS_UPDATE_INTERVAL(Const.STUDENT_ACTIVITY_LOGS_UPDATE_INTERVAL.toMinutes()); + // CHECKSTYLE.ON:JavadocVariable private final Object value; diff --git a/src/main/java/teammates/ui/constants/ApiStringConst.java b/src/main/java/teammates/ui/constants/ApiStringConst.java new file mode 100644 index 000000000000..5158f2ac9643 --- /dev/null +++ b/src/main/java/teammates/ui/constants/ApiStringConst.java @@ -0,0 +1,40 @@ +package teammates.ui.constants; + +import com.fasterxml.jackson.annotation.JsonValue; + +import teammates.common.util.FieldValidator; + +/** + * Special constants used by the back-end. + */ +public enum ApiStringConst { + // CHECKSTYLE.OFF:JavadocVariable + EMAIL_REGEX(escapeRegex(FieldValidator.REGEX_EMAIL)); + // CHECKSTYLE.ON:JavadocVariable + + private final Object value; + + ApiStringConst(Object value) { + this.value = value; + } + + @JsonValue + public Object getValue() { + return value; + } + + /** + * Escape regex pattern strings to ensure the pattern remains valid when converted to JS. + */ + private static String escapeRegex(String regexStr) { + String escapedRegexStr = regexStr; + // Double escape backslashes + escapedRegexStr = escapedRegexStr.replace("\\", "\\\\"); + // Replace possessive zero or more times quantifier *+ that the email pattern uses + // with greedy zero or more times quantifier * + // as possessive quantifiers are not supported in JavaScript + escapedRegexStr = escapedRegexStr.replace("*+", "*"); + return escapedRegexStr; + } + +} diff --git a/src/main/java/teammates/ui/constants/ResourceEndpoints.java b/src/main/java/teammates/ui/constants/ResourceEndpoints.java index 2401fad8efe2..3a7a3abe88c0 100644 --- a/src/main/java/teammates/ui/constants/ResourceEndpoints.java +++ b/src/main/java/teammates/ui/constants/ResourceEndpoints.java @@ -15,7 +15,9 @@ public enum ResourceEndpoints { ACCOUNT(ResourceURIs.ACCOUNT), ACCOUNT_RESET(ResourceURIs.ACCOUNT_RESET), ACCOUNT_REQUEST(ResourceURIs.ACCOUNT_REQUEST), + ACCOUNT_REQUESTS(ResourceURIs.ACCOUNT_REQUESTS), ACCOUNT_REQUEST_RESET(ResourceURIs.ACCOUNT_REQUEST_RESET), + ACCOUNT_REQUEST_REJECT(ResourceURIs.ACCOUNT_REQUEST_REJECTION), ACCOUNTS(ResourceURIs.ACCOUNTS), RESPONSE_COMMENT(ResourceURIs.RESPONSE_COMMENT), COURSE(ResourceURIs.COURSE), diff --git a/src/main/java/teammates/ui/output/AccountRequestData.java b/src/main/java/teammates/ui/output/AccountRequestData.java index 2d50bcb13600..ee006ac20356 100644 --- a/src/main/java/teammates/ui/output/AccountRequestData.java +++ b/src/main/java/teammates/ui/output/AccountRequestData.java @@ -1,7 +1,8 @@ package teammates.ui.output; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.datatransfer.attributes.AccountRequestAttributes; import teammates.storage.sqlentity.AccountRequest; @@ -9,36 +10,44 @@ * Output format of account request data. */ public class AccountRequestData extends ApiOutput { - + private final String id; private final String email; private final String name; private final String institute; private final String registrationKey; + private final AccountRequestStatus status; + @Nullable + private final String comments; @Nullable private final Long registeredAt; private final long createdAt; public AccountRequestData(AccountRequestAttributes accountRequestInfo) { - + this.id = accountRequestInfo.getId(); this.name = accountRequestInfo.getName(); this.email = accountRequestInfo.getEmail(); this.institute = accountRequestInfo.getInstitute(); this.registrationKey = accountRequestInfo.getRegistrationKey(); + this.comments = null; this.createdAt = accountRequestInfo.getCreatedAt().toEpochMilli(); if (accountRequestInfo.getRegisteredAt() == null) { + this.status = AccountRequestStatus.APPROVED; this.registeredAt = null; } else { + this.status = AccountRequestStatus.REGISTERED; this.registeredAt = accountRequestInfo.getRegisteredAt().toEpochMilli(); } } public AccountRequestData(AccountRequest accountRequest) { - + this.id = accountRequest.getId().toString(); this.name = accountRequest.getName(); this.email = accountRequest.getEmail(); this.institute = accountRequest.getInstitute(); this.registrationKey = accountRequest.getRegistrationKey(); + this.status = accountRequest.getStatus(); + this.comments = accountRequest.getComments(); this.createdAt = accountRequest.getCreatedAt().toEpochMilli(); if (accountRequest.getRegisteredAt() == null) { @@ -48,6 +57,10 @@ public AccountRequestData(AccountRequest accountRequest) { } } + public String getId() { + return id; + } + public String getInstitute() { return institute; } @@ -64,6 +77,14 @@ public String getRegistrationKey() { return registrationKey; } + public AccountRequestStatus getStatus() { + return status; + } + + public String getComments() { + return comments; + } + public Long getRegisteredAt() { return registeredAt; } diff --git a/src/main/java/teammates/ui/output/ApiOutput.java b/src/main/java/teammates/ui/output/ApiOutput.java index f5bdc5ca1f1c..eaf4ea4f0a86 100644 --- a/src/main/java/teammates/ui/output/ApiOutput.java +++ b/src/main/java/teammates/ui/output/ApiOutput.java @@ -1,6 +1,6 @@ package teammates.ui.output; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; /** * Generic output format for all API requests. diff --git a/src/main/java/teammates/ui/output/AuthInfo.java b/src/main/java/teammates/ui/output/AuthInfo.java index 696b2bbed631..91f78627ccea 100644 --- a/src/main/java/teammates/ui/output/AuthInfo.java +++ b/src/main/java/teammates/ui/output/AuthInfo.java @@ -1,6 +1,6 @@ package teammates.ui.output; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.UserInfo; diff --git a/src/main/java/teammates/ui/output/CourseData.java b/src/main/java/teammates/ui/output/CourseData.java index b2ce63344838..b5787bf2f48c 100644 --- a/src/main/java/teammates/ui/output/CourseData.java +++ b/src/main/java/teammates/ui/output/CourseData.java @@ -1,6 +1,6 @@ package teammates.ui.output; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.InstructorPermissionSet; import teammates.common.datatransfer.attributes.CourseAttributes; @@ -15,6 +15,8 @@ public class CourseData extends ApiOutput { private final String courseName; private final String timeZone; private final String institute; + @Nullable + private final Boolean isMigrated; private long creationTimestamp; private long deletionTimestamp; @Nullable @@ -29,6 +31,7 @@ public CourseData(CourseAttributes courseAttributes) { if (courseAttributes.getDeletedAt() != null) { this.deletionTimestamp = courseAttributes.getDeletedAt().toEpochMilli(); } + this.isMigrated = false; } public CourseData(Course course) { @@ -40,6 +43,7 @@ public CourseData(Course course) { if (course.getDeletedAt() != null) { this.deletionTimestamp = course.getDeletedAt().toEpochMilli(); } + this.isMigrated = true; } public String getCourseId() { @@ -66,6 +70,10 @@ public long getDeletionTimestamp() { return deletionTimestamp; } + public Boolean getIsMigrated() { + return isMigrated; + } + public InstructorPermissionSet getPrivileges() { return privileges; } diff --git a/src/main/java/teammates/ui/output/FeedbackQuestionRecipientData.java b/src/main/java/teammates/ui/output/FeedbackQuestionRecipientData.java index 5acd6b0457f9..02a07f625a6b 100644 --- a/src/main/java/teammates/ui/output/FeedbackQuestionRecipientData.java +++ b/src/main/java/teammates/ui/output/FeedbackQuestionRecipientData.java @@ -1,6 +1,6 @@ package teammates.ui.output; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.FeedbackQuestionRecipient; diff --git a/src/main/java/teammates/ui/output/FeedbackResponseData.java b/src/main/java/teammates/ui/output/FeedbackResponseData.java index 4aa5d084c647..c6b49db91ed7 100644 --- a/src/main/java/teammates/ui/output/FeedbackResponseData.java +++ b/src/main/java/teammates/ui/output/FeedbackResponseData.java @@ -1,6 +1,6 @@ package teammates.ui.output; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.attributes.FeedbackResponseAttributes; import teammates.common.datatransfer.questions.FeedbackResponseDetails; diff --git a/src/main/java/teammates/ui/output/FeedbackSessionData.java b/src/main/java/teammates/ui/output/FeedbackSessionData.java index 9b529f1b5b2f..a0b2f46b0500 100644 --- a/src/main/java/teammates/ui/output/FeedbackSessionData.java +++ b/src/main/java/teammates/ui/output/FeedbackSessionData.java @@ -3,9 +3,10 @@ import java.time.Instant; import java.util.HashMap; import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.InstructorPermissionSet; import teammates.common.datatransfer.attributes.FeedbackSessionAttributes; @@ -20,6 +21,10 @@ * The API output format of {@link FeedbackSessionAttributes}. */ public class FeedbackSessionData extends ApiOutput { + + @Nullable + private final UUID feedbackSessionId; + private final String courseId; private final String timeZone; private final String feedbackSessionName; @@ -60,6 +65,7 @@ public class FeedbackSessionData extends ApiOutput { public FeedbackSessionData(FeedbackSessionAttributes feedbackSessionAttributes) { String timeZone = feedbackSessionAttributes.getTimeZone(); + this.feedbackSessionId = null; this.courseId = feedbackSessionAttributes.getCourseId(); this.timeZone = timeZone; this.feedbackSessionName = feedbackSessionAttributes.getFeedbackSessionName(); @@ -148,6 +154,7 @@ public FeedbackSessionData(FeedbackSession feedbackSession) { assert feedbackSession != null; assert feedbackSession.getCourse() != null; String timeZone = feedbackSession.getCourse().getTimeZone(); + this.feedbackSessionId = feedbackSession.getId(); this.courseId = feedbackSession.getCourse().getId(); this.timeZone = timeZone; this.feedbackSessionName = feedbackSession.getName(); @@ -252,6 +259,10 @@ public FeedbackSessionData(FeedbackSession feedbackSession, Instant extendedDead } } + public UUID getFeedbackSessionId() { + return feedbackSessionId; + } + public String getCourseId() { return courseId; } diff --git a/src/main/java/teammates/ui/output/FeedbackSessionLogData.java b/src/main/java/teammates/ui/output/FeedbackSessionLogData.java index 0825b0b58946..7afc398f0bff 100644 --- a/src/main/java/teammates/ui/output/FeedbackSessionLogData.java +++ b/src/main/java/teammates/ui/output/FeedbackSessionLogData.java @@ -8,6 +8,7 @@ import teammates.common.datatransfer.attributes.FeedbackSessionAttributes; import teammates.common.datatransfer.attributes.StudentAttributes; import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; import teammates.storage.sqlentity.Student; /** @@ -17,19 +18,24 @@ public class FeedbackSessionLogData { private final FeedbackSessionData feedbackSessionData; private final List feedbackSessionLogEntries; - // Remove generic types after migration is done (i.e. can just use FeedbackSession and Student) - public FeedbackSessionLogData(S feedbackSession, List logEntries, + // Remove generic types after migration is done (i.e. can just use FeedbackSession, Student, FeedbackSessionLog) + public FeedbackSessionLogData(S feedbackSession, List logEntries, Map studentsMap) { if (feedbackSession instanceof FeedbackSessionAttributes) { FeedbackSessionAttributes fs = (FeedbackSessionAttributes) feedbackSession; FeedbackSessionData fsData = new FeedbackSessionData(fs); List fsLogEntryDatas = logEntries.stream() .map(log -> { - T student = studentsMap.get(log.getStudentEmail()); - if (student instanceof StudentAttributes) { - return new FeedbackSessionLogEntryData(log, (StudentAttributes) student); + if (log instanceof FeedbackSessionLogEntry) { + FeedbackSessionLogEntry convertedLog = (FeedbackSessionLogEntry) log; + T student = studentsMap.get(convertedLog.getStudentEmail()); + if (student instanceof StudentAttributes) { + return new FeedbackSessionLogEntryData(convertedLog, (StudentAttributes) student); + } else { + throw new IllegalArgumentException("Invalid student type"); + } } else { - throw new IllegalArgumentException("Invalid student type"); + throw new IllegalArgumentException("Invalid log type"); } }) .collect(Collectors.toList()); @@ -40,11 +46,16 @@ public FeedbackSessionLogData(S feedbackSession, List fsLogEntryDatas = logEntries.stream() .map(log -> { - T student = studentsMap.get(log.getStudentEmail()); - if (student instanceof Student) { - return new FeedbackSessionLogEntryData(log, (Student) student); + if (log instanceof FeedbackSessionLog) { + FeedbackSessionLog convertedLog = (FeedbackSessionLog) log; + T student = studentsMap.get(convertedLog.getStudent().getEmail()); + if (student instanceof Student) { + return new FeedbackSessionLogEntryData(convertedLog, (Student) student); + } else { + throw new IllegalArgumentException("Invalid student type"); + } } else { - throw new IllegalArgumentException("Invalid student type"); + throw new IllegalArgumentException("Invalid log type"); } }) .collect(Collectors.toList()); diff --git a/src/main/java/teammates/ui/output/FeedbackSessionLogEntryData.java b/src/main/java/teammates/ui/output/FeedbackSessionLogEntryData.java index a70eaa7b5054..99669d10e336 100644 --- a/src/main/java/teammates/ui/output/FeedbackSessionLogEntryData.java +++ b/src/main/java/teammates/ui/output/FeedbackSessionLogEntryData.java @@ -3,6 +3,7 @@ import teammates.common.datatransfer.FeedbackSessionLogEntry; import teammates.common.datatransfer.attributes.StudentAttributes; import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.storage.sqlentity.FeedbackSessionLog; import teammates.storage.sqlentity.Student; /** @@ -22,10 +23,10 @@ public FeedbackSessionLogEntryData(FeedbackSessionLogEntry logEntry, StudentAttr this.timestamp = timestamp; } - public FeedbackSessionLogEntryData(FeedbackSessionLogEntry logEntry, Student student) { + public FeedbackSessionLogEntryData(FeedbackSessionLog logEntry, Student student) { StudentData studentData = new StudentData(student); - FeedbackSessionLogType logType = FeedbackSessionLogType.valueOfLabel(logEntry.getFeedbackSessionLogType()); - long timestamp = logEntry.getTimestamp(); + FeedbackSessionLogType logType = logEntry.getFeedbackSessionLogType(); + long timestamp = logEntry.getTimestamp().toEpochMilli(); this.studentData = studentData; this.feedbackSessionLogType = logType; this.timestamp = timestamp; diff --git a/src/main/java/teammates/ui/output/FeedbackSessionLogsData.java b/src/main/java/teammates/ui/output/FeedbackSessionLogsData.java index 3926e252817e..b6f722dc7709 100644 --- a/src/main/java/teammates/ui/output/FeedbackSessionLogsData.java +++ b/src/main/java/teammates/ui/output/FeedbackSessionLogsData.java @@ -4,8 +4,6 @@ import java.util.Map; import java.util.stream.Collectors; -import teammates.common.datatransfer.FeedbackSessionLogEntry; - /** * The API output format for logs on all feedback sessions in a course. */ @@ -13,13 +11,13 @@ public class FeedbackSessionLogsData extends ApiOutput { private final List feedbackSessionLogs; - // Remove generic types after migration is done (i.e. can just use FeedbackSession and Student) - public FeedbackSessionLogsData(Map> groupedEntries, + // Remove generic types after migration is done (i.e. can just use FeedbackSession and Student, FeedbackSessionLog) + public FeedbackSessionLogsData(Map> groupedEntries, Map studentsMap, Map sessionsMap) { this.feedbackSessionLogs = groupedEntries.entrySet().stream() .map(entry -> { T feedbackSession = sessionsMap.get(entry.getKey()); - List logEntries = entry.getValue(); + List logEntries = entry.getValue(); return new FeedbackSessionLogData(feedbackSession, logEntries, studentsMap); }) .collect(Collectors.toList()); diff --git a/src/main/java/teammates/ui/output/HasResponsesData.java b/src/main/java/teammates/ui/output/HasResponsesData.java index aec14a88dac8..1ffe6ff36f31 100644 --- a/src/main/java/teammates/ui/output/HasResponsesData.java +++ b/src/main/java/teammates/ui/output/HasResponsesData.java @@ -3,7 +3,7 @@ import java.util.HashMap; import java.util.Map; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; /** * The API output format to represent if there are responses. diff --git a/src/main/java/teammates/ui/output/InstructorData.java b/src/main/java/teammates/ui/output/InstructorData.java index e185028ce1ee..bb2f4b14499f 100644 --- a/src/main/java/teammates/ui/output/InstructorData.java +++ b/src/main/java/teammates/ui/output/InstructorData.java @@ -1,6 +1,6 @@ package teammates.ui.output; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.InstructorPermissionRole; import teammates.common.datatransfer.attributes.InstructorAttributes; diff --git a/src/main/java/teammates/ui/output/SessionResultsData.java b/src/main/java/teammates/ui/output/SessionResultsData.java index 796aba295585..edb63924ee1a 100644 --- a/src/main/java/teammates/ui/output/SessionResultsData.java +++ b/src/main/java/teammates/ui/output/SessionResultsData.java @@ -9,7 +9,7 @@ import java.util.Queue; import java.util.Set; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.CourseRoster; import teammates.common.datatransfer.FeedbackParticipantType; @@ -718,7 +718,7 @@ public List getQuestions() { /** * API output format for questions in session results. */ - public static class QuestionOutput { + public static final class QuestionOutput { private final FeedbackQuestionData feedbackQuestion; private final String questionStatistics; @@ -785,7 +785,7 @@ public List> getOtherResponses() { /** * API output format for question responses. */ - public static class ResponseOutput { + public static final class ResponseOutput { private boolean isMissingResponse; @@ -989,7 +989,7 @@ ResponseOutput build() { /** * API output format for response comments. */ - public static class CommentOutput extends FeedbackResponseCommentData { + public static final class CommentOutput extends FeedbackResponseCommentData { @Nullable private String commentGiverName; diff --git a/src/main/java/teammates/ui/output/StudentData.java b/src/main/java/teammates/ui/output/StudentData.java index 933b9e9d0254..162ea0bf5a58 100644 --- a/src/main/java/teammates/ui/output/StudentData.java +++ b/src/main/java/teammates/ui/output/StudentData.java @@ -1,6 +1,8 @@ package teammates.ui.output; -import javax.annotation.Nullable; +import java.util.UUID; + +import jakarta.annotation.Nullable; import teammates.common.datatransfer.attributes.StudentAttributes; import teammates.storage.sqlentity.Student; @@ -10,6 +12,9 @@ */ public class StudentData extends ApiOutput { + @Nullable + private final UUID studentId; + private final String email; private final String courseId; @@ -29,6 +34,7 @@ public class StudentData extends ApiOutput { private final String sectionName; public StudentData(StudentAttributes studentAttributes) { + this.studentId = null; this.email = studentAttributes.getEmail(); this.courseId = studentAttributes.getCourse(); this.name = studentAttributes.getName(); @@ -39,6 +45,7 @@ public StudentData(StudentAttributes studentAttributes) { } public StudentData(Student student) { + this.studentId = student.getId(); this.email = student.getEmail(); this.courseId = student.getCourseId(); this.name = student.getName(); @@ -48,6 +55,10 @@ public StudentData(Student student) { this.sectionName = student.getSectionName(); } + public UUID getStudentId() { + return studentId; + } + public String getEmail() { return email; } diff --git a/src/main/java/teammates/ui/request/AccountCreateRequest.java b/src/main/java/teammates/ui/request/AccountCreateRequest.java index f3097ce11521..3e63e750b9bf 100644 --- a/src/main/java/teammates/ui/request/AccountCreateRequest.java +++ b/src/main/java/teammates/ui/request/AccountCreateRequest.java @@ -3,6 +3,8 @@ import java.util.ArrayList; import java.util.List; +import jakarta.annotation.Nullable; + import teammates.common.util.FieldValidator; import teammates.common.util.StringHelper; @@ -14,6 +16,8 @@ public class AccountCreateRequest extends BasicRequest { private String instructorEmail; private String instructorName; private String instructorInstitution; + @Nullable + private String instructorComments; public String getInstructorEmail() { return instructorEmail; @@ -27,6 +31,10 @@ public String getInstructorInstitution() { return this.instructorInstitution; } + public String getInstructorComments() { + return this.instructorComments; + } + public void setInstructorName(String name) { this.instructorName = name; } @@ -39,6 +47,10 @@ public void setInstructorEmail(String instructorEmail) { this.instructorEmail = instructorEmail; } + public void setInstructorComments(String instructorComments) { + this.instructorComments = instructorComments; + } + @Override public void validate() throws InvalidHttpRequestBodyException { assertTrue(this.instructorEmail != null, "email cannot be null"); diff --git a/src/main/java/teammates/ui/request/AccountRequestRejectionRequest.java b/src/main/java/teammates/ui/request/AccountRequestRejectionRequest.java new file mode 100644 index 000000000000..d432c629eeb6 --- /dev/null +++ b/src/main/java/teammates/ui/request/AccountRequestRejectionRequest.java @@ -0,0 +1,46 @@ +package teammates.ui.request; + +import java.util.Objects; + +import jakarta.annotation.Nullable; + +import teammates.common.util.SanitizationHelper; + +/** + * The request reasonBody for rejecting an account request. + */ +public class AccountRequestRejectionRequest extends BasicRequest { + @Nullable + private String reasonTitle; + + @Nullable + private String reasonBody; + + public AccountRequestRejectionRequest(String reasonTitle, String reasonBody) { + this.reasonTitle = SanitizationHelper.sanitizeTitle(reasonTitle); + this.reasonBody = SanitizationHelper.sanitizeForRichText(reasonBody); + } + + @Override + public void validate() throws InvalidHttpRequestBodyException { + if (reasonBody == null || reasonTitle == null) { + assertTrue(Objects.equals(reasonBody, reasonTitle), + "Both reason body and title need to be null to reject silently"); + } + } + + public String getReasonTitle() { + return this.reasonTitle; + } + + public String getReasonBody() { + return this.reasonBody; + } + + /** + * Returns true if both reason body and title are non-null. + */ + public boolean checkHasReason() { + return this.reasonBody != null && this.reasonTitle != null; + } +} diff --git a/src/main/java/teammates/ui/request/AccountRequestUpdateRequest.java b/src/main/java/teammates/ui/request/AccountRequestUpdateRequest.java new file mode 100644 index 000000000000..39895cf5ef2e --- /dev/null +++ b/src/main/java/teammates/ui/request/AccountRequestUpdateRequest.java @@ -0,0 +1,63 @@ +package teammates.ui.request; + +import jakarta.annotation.Nullable; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.util.SanitizationHelper; + +/** + * The create request for an account request update request. + */ +public class AccountRequestUpdateRequest extends BasicRequest { + private String name; + private String email; + private String institute; + private AccountRequestStatus status; + + @Nullable + private String comments; + + public AccountRequestUpdateRequest(String name, String email, String institute, AccountRequestStatus status, + String comments) { + this.name = SanitizationHelper.sanitizeName(name); + this.email = SanitizationHelper.sanitizeEmail(email); + this.institute = SanitizationHelper.sanitizeName(institute); + this.status = status; + if (comments != null) { + this.comments = SanitizationHelper.sanitizeTextField(comments); + } + } + + @Override + public void validate() throws InvalidHttpRequestBodyException { + assertTrue(name != null, "name cannot be null"); + assertTrue(email != null, "email cannot be null"); + assertTrue(institute != null, "institute cannot be null"); + assertTrue(status != null, "status cannot be null"); + assertTrue(status == AccountRequestStatus.APPROVED + || status == AccountRequestStatus.REJECTED + || status == AccountRequestStatus.PENDING + || status == AccountRequestStatus.REGISTERED, + "status must be one of the following: APPROVED, REJECTED, PENDING, REGISTERED"); + } + + public String getName() { + return this.name; + } + + public String getEmail() { + return this.email; + } + + public String getInstitute() { + return this.institute; + } + + public AccountRequestStatus getStatus() { + return this.status; + } + + public String getComments() { + return this.comments; + } +} diff --git a/src/main/java/teammates/ui/request/FeedbackSessionBasicRequest.java b/src/main/java/teammates/ui/request/FeedbackSessionBasicRequest.java index f1b0b42ed961..a564d1e5616b 100644 --- a/src/main/java/teammates/ui/request/FeedbackSessionBasicRequest.java +++ b/src/main/java/teammates/ui/request/FeedbackSessionBasicRequest.java @@ -3,7 +3,7 @@ import java.time.Duration; import java.time.Instant; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.util.Const; import teammates.ui.output.ResponseVisibleSetting; diff --git a/src/main/java/teammates/ui/request/FeedbackSessionCreateRequest.java b/src/main/java/teammates/ui/request/FeedbackSessionCreateRequest.java index fce6ffcf963b..b92dcc63431b 100644 --- a/src/main/java/teammates/ui/request/FeedbackSessionCreateRequest.java +++ b/src/main/java/teammates/ui/request/FeedbackSessionCreateRequest.java @@ -1,6 +1,6 @@ package teammates.ui.request; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; /** * The request body format for creation of feedback session. diff --git a/src/main/java/teammates/ui/request/InstructorCreateRequest.java b/src/main/java/teammates/ui/request/InstructorCreateRequest.java index f73841665b0c..b8bccd89744e 100644 --- a/src/main/java/teammates/ui/request/InstructorCreateRequest.java +++ b/src/main/java/teammates/ui/request/InstructorCreateRequest.java @@ -1,6 +1,6 @@ package teammates.ui.request; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import teammates.common.datatransfer.InstructorPermissionRole; diff --git a/src/main/java/teammates/ui/servlets/AuthServlet.java b/src/main/java/teammates/ui/servlets/AuthServlet.java index 45f55b1d858d..1cae38e6cc4c 100644 --- a/src/main/java/teammates/ui/servlets/AuthServlet.java +++ b/src/main/java/teammates/ui/servlets/AuthServlet.java @@ -4,9 +4,9 @@ import java.util.Arrays; import java.util.List; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; import com.google.api.client.auth.oauth2.AuthorizationCodeFlow; import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; diff --git a/src/main/java/teammates/ui/servlets/DevServerLoginServlet.java b/src/main/java/teammates/ui/servlets/DevServerLoginServlet.java index db8f8ebf321f..d36082f523cd 100644 --- a/src/main/java/teammates/ui/servlets/DevServerLoginServlet.java +++ b/src/main/java/teammates/ui/servlets/DevServerLoginServlet.java @@ -3,9 +3,9 @@ import java.io.IOException; import java.io.PrintWriter; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus; diff --git a/src/main/java/teammates/ui/servlets/HealthCheckServlet.java b/src/main/java/teammates/ui/servlets/HealthCheckServlet.java index f6073cd67570..2488c3c7cb4d 100644 --- a/src/main/java/teammates/ui/servlets/HealthCheckServlet.java +++ b/src/main/java/teammates/ui/servlets/HealthCheckServlet.java @@ -2,9 +2,9 @@ import java.io.IOException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus; diff --git a/src/main/java/teammates/ui/servlets/HibernateContextListener.java b/src/main/java/teammates/ui/servlets/HibernateContextListener.java index 9497dbae1f24..f3dc35ea6ea2 100644 --- a/src/main/java/teammates/ui/servlets/HibernateContextListener.java +++ b/src/main/java/teammates/ui/servlets/HibernateContextListener.java @@ -1,7 +1,7 @@ package teammates.ui.servlets; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; import teammates.common.util.Config; import teammates.common.util.HibernateUtil; diff --git a/src/main/java/teammates/ui/servlets/LegacyUrlMapper.java b/src/main/java/teammates/ui/servlets/LegacyUrlMapper.java index eb905b9b5f17..b1ecb2a1f16d 100644 --- a/src/main/java/teammates/ui/servlets/LegacyUrlMapper.java +++ b/src/main/java/teammates/ui/servlets/LegacyUrlMapper.java @@ -2,9 +2,9 @@ import java.io.IOException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus; diff --git a/src/main/java/teammates/ui/servlets/LoginServlet.java b/src/main/java/teammates/ui/servlets/LoginServlet.java index a2bfeb76bfad..14c03632ea8d 100644 --- a/src/main/java/teammates/ui/servlets/LoginServlet.java +++ b/src/main/java/teammates/ui/servlets/LoginServlet.java @@ -2,8 +2,8 @@ import java.io.IOException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus; diff --git a/src/main/java/teammates/ui/servlets/LogoutServlet.java b/src/main/java/teammates/ui/servlets/LogoutServlet.java index 9a5ac4a3c49b..dcfde659e435 100644 --- a/src/main/java/teammates/ui/servlets/LogoutServlet.java +++ b/src/main/java/teammates/ui/servlets/LogoutServlet.java @@ -2,9 +2,9 @@ import java.io.IOException; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus; @@ -28,6 +28,8 @@ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOExc if (frontendUrl == null) { frontendUrl = ""; } + // Prevent HTTP response splitting + frontendUrl = resp.encodeRedirectURL(frontendUrl.replace("\r\n", "")); log.request(req, HttpStatus.SC_MOVED_TEMPORARILY, "Redirect to home page after logging out"); resp.sendRedirect(frontendUrl + "/web"); } diff --git a/src/main/java/teammates/ui/servlets/OAuth2CallbackServlet.java b/src/main/java/teammates/ui/servlets/OAuth2CallbackServlet.java index abad67f4862f..c83cd485d20c 100644 --- a/src/main/java/teammates/ui/servlets/OAuth2CallbackServlet.java +++ b/src/main/java/teammates/ui/servlets/OAuth2CallbackServlet.java @@ -5,9 +5,9 @@ import java.net.URISyntaxException; import java.util.Map; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus; @@ -158,7 +158,7 @@ private void logAndPrintError(HttpServletRequest req, HttpServletResponse resp, log.request(req, status, message); } - private static class AuthResult { + private static final class AuthResult { private final String email; private final String nextUrl; diff --git a/src/main/java/teammates/ui/servlets/ObjectifyFilter.java b/src/main/java/teammates/ui/servlets/ObjectifyFilter.java new file mode 100644 index 000000000000..faca19c54313 --- /dev/null +++ b/src/main/java/teammates/ui/servlets/ObjectifyFilter.java @@ -0,0 +1,30 @@ +package teammates.ui.servlets; + +import java.io.Closeable; +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import com.googlecode.objectify.ObjectifyService; + +/** + * Filter to activate Objectify service. + * + *

    This is adapted from the official ObjectifyFilter class + * with modification to support jakarta.servlet in place of javax.servlet. + * + * @see https://github.com/objectify/objectify/blob/6.0.7/src/main/java/com/googlecode/objectify/ObjectifyFilter.java + */ +public class ObjectifyFilter implements Filter { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + try (Closeable ignored = ObjectifyService.begin()) { + chain.doFilter(request, response); + } + } +} diff --git a/src/main/java/teammates/ui/servlets/OriginCheckFilter.java b/src/main/java/teammates/ui/servlets/OriginCheckFilter.java index 4d6cfeb6e0d2..cbd1e8d7b9aa 100644 --- a/src/main/java/teammates/ui/servlets/OriginCheckFilter.java +++ b/src/main/java/teammates/ui/servlets/OriginCheckFilter.java @@ -3,14 +3,13 @@ import java.io.IOException; import java.util.Arrays; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpDelete; @@ -49,11 +48,6 @@ public class OriginCheckFilter implements Filter { "ngsw-bypass" )); - @Override - public void init(FilterConfig filterConfig) { - // nothing to do - } - @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; @@ -73,7 +67,7 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) } // The header X-AppEngine-QueueName cannot be spoofed as GAE will strip any user-sent X-AppEngine-QueueName headers. - // Reference: https://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-handlers#reading_request_headers + // Reference: https://cloud.google.com/tasks/docs/creating-appengine-handlers#reading_task_request_headers boolean isRequestFromAppEngineQueue = request.getHeader("X-AppEngine-QueueName") != null; if (isRequestFromAppEngineQueue) { @@ -181,9 +175,4 @@ private void denyAccess(String message, HttpServletRequest request, HttpServletR log.request(request, statusCode, message); } - @Override - public void destroy() { - // nothing to do - } - } diff --git a/src/main/java/teammates/ui/servlets/RequestTraceFilter.java b/src/main/java/teammates/ui/servlets/RequestTraceFilter.java index 75ba7fcfd8e6..234640ee3239 100644 --- a/src/main/java/teammates/ui/servlets/RequestTraceFilter.java +++ b/src/main/java/teammates/ui/servlets/RequestTraceFilter.java @@ -3,14 +3,13 @@ import java.io.IOException; import java.util.Random; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.codec.binary.Hex; import org.apache.http.HttpStatus; @@ -27,11 +26,6 @@ public class RequestTraceFilter implements Filter { private static final Logger log = Logger.getLogger(); - @Override - public void init(FilterConfig filterConfig) { - // nothing to do - } - @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) resp; @@ -60,7 +54,7 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain } // The header X-AppEngine-QueueName cannot be spoofed as GAE will strip any user-sent X-AppEngine-QueueName headers. - // Reference: https://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-handlers#reading_request_headers + // Reference: https://cloud.google.com/tasks/docs/creating-appengine-handlers#reading_task_request_headers boolean isRequestFromAppEngineQueue = request.getHeader("X-AppEngine-QueueName") != null; // GAE will terminate an instance if any request exceeds 10 minutes. @@ -93,11 +87,6 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain chain.doFilter(req, resp); } - @Override - public void destroy() { - // nothing to do - } - private void throwError(HttpServletRequest req, HttpServletResponse resp, int statusCode, String message) throws IOException { JsonResult result = new JsonResult(message, statusCode); diff --git a/src/main/java/teammates/ui/servlets/WebApiServlet.java b/src/main/java/teammates/ui/servlets/WebApiServlet.java index f4ee00059d82..eaeac90d3993 100644 --- a/src/main/java/teammates/ui/servlets/WebApiServlet.java +++ b/src/main/java/teammates/ui/servlets/WebApiServlet.java @@ -2,9 +2,9 @@ import java.io.IOException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus; import org.hibernate.HibernateException; diff --git a/src/main/java/teammates/ui/servlets/WebPageServlet.java b/src/main/java/teammates/ui/servlets/WebPageServlet.java index e36f26b56e26..ec5bb560b9e3 100644 --- a/src/main/java/teammates/ui/servlets/WebPageServlet.java +++ b/src/main/java/teammates/ui/servlets/WebPageServlet.java @@ -2,10 +2,10 @@ import java.io.IOException; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus; diff --git a/src/main/java/teammates/ui/servlets/WebSecurityHeaderFilter.java b/src/main/java/teammates/ui/servlets/WebSecurityHeaderFilter.java index 707753568c74..2158f6ecf462 100644 --- a/src/main/java/teammates/ui/servlets/WebSecurityHeaderFilter.java +++ b/src/main/java/teammates/ui/servlets/WebSecurityHeaderFilter.java @@ -3,13 +3,12 @@ import java.io.IOException; import java.util.Arrays; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; import teammates.common.util.Config; @@ -36,11 +35,6 @@ public class WebSecurityHeaderFilter implements Filter { "base-uri 'self'" )); - @Override - public void init(FilterConfig filterConfig) { - // nothing to do - } - @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { @@ -54,9 +48,4 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha chain.doFilter(request, resp); } - @Override - public void destroy() { - // nothing to do - } - } diff --git a/src/main/java/teammates/ui/webapi/AccountRequestSearchIndexingWorkerAction.java b/src/main/java/teammates/ui/webapi/AccountRequestSearchIndexingWorkerAction.java index e543b012db45..214ca01f8fd7 100644 --- a/src/main/java/teammates/ui/webapi/AccountRequestSearchIndexingWorkerAction.java +++ b/src/main/java/teammates/ui/webapi/AccountRequestSearchIndexingWorkerAction.java @@ -1,5 +1,7 @@ package teammates.ui.webapi; +import java.util.UUID; + import org.apache.http.HttpStatus; import teammates.common.exception.SearchServiceException; @@ -13,10 +15,16 @@ public class AccountRequestSearchIndexingWorkerAction extends AdminOnlyAction { @Override public ActionResult execute() { - String email = getNonNullRequestParamValue(ParamsNames.INSTRUCTOR_EMAIL); - String institute = getNonNullRequestParamValue(ParamsNames.INSTRUCTOR_INSTITUTION); + String id = getNonNullRequestParamValue(ParamsNames.ACCOUNT_REQUEST_ID); + UUID accountRequestId; + + try { + accountRequestId = UUID.fromString(id); + } catch (IllegalArgumentException e) { + throw new InvalidHttpParameterException(e.getMessage(), e); + } - AccountRequest accRequest = sqlLogic.getAccountRequest(email, institute); + AccountRequest accRequest = sqlLogic.getAccountRequest(accountRequestId); try { sqlLogic.putAccountRequestDocument(accRequest); diff --git a/src/main/java/teammates/ui/webapi/Action.java b/src/main/java/teammates/ui/webapi/Action.java index ab32d4ed21d6..773c7f6f6cf1 100644 --- a/src/main/java/teammates/ui/webapi/Action.java +++ b/src/main/java/teammates/ui/webapi/Action.java @@ -4,7 +4,7 @@ import java.util.Optional; import java.util.UUID; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import teammates.common.datatransfer.InstructorPermissionSet; import teammates.common.datatransfer.UserInfo; diff --git a/src/main/java/teammates/ui/webapi/ActionFactory.java b/src/main/java/teammates/ui/webapi/ActionFactory.java index 38c4b00b753e..9a144adb3680 100644 --- a/src/main/java/teammates/ui/webapi/ActionFactory.java +++ b/src/main/java/teammates/ui/webapi/ActionFactory.java @@ -3,7 +3,7 @@ import java.util.HashMap; import java.util.Map; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpDelete; @@ -50,7 +50,10 @@ public final class ActionFactory { map(ResourceURIs.ACCOUNT_REQUEST, GET, GetAccountRequestAction.class); map(ResourceURIs.ACCOUNT_REQUEST, POST, CreateAccountRequestAction.class); map(ResourceURIs.ACCOUNT_REQUEST, DELETE, DeleteAccountRequestAction.class); + map(ResourceURIs.ACCOUNT_REQUEST, PUT, UpdateAccountRequestAction.class); + map(ResourceURIs.ACCOUNT_REQUESTS, GET, GetAccountRequestsAction.class); map(ResourceURIs.ACCOUNT_REQUEST_RESET, PUT, ResetAccountRequestAction.class); + map(ResourceURIs.ACCOUNT_REQUEST_REJECTION, POST, RejectAccountRequestAction.class); map(ResourceURIs.ACCOUNTS, GET, GetAccountsAction.class); map(ResourceURIs.COURSE, GET, GetCourseAction.class); map(ResourceURIs.COURSE, DELETE, DeleteCourseAction.class); @@ -140,7 +143,7 @@ public final class ActionFactory { map(ResourceURIs.ACTION_CLASS, GET, GetActionClassesAction.class); // Cron jobs; use GET request - // Reference: https://cloud.google.com/appengine/docs/standard/java11/scheduling-jobs-with-cron-yaml + // Reference: https://cloud.google.com/appengine/docs/standard/scheduling-jobs-with-cron-yaml map(CronJobURIs.AUTOMATED_LOG_COMPILATION, GET, CompileLogsAction.class); map(CronJobURIs.AUTOMATED_DATASTORE_BACKUP, GET, DatastoreBackupAction.class); @@ -151,6 +154,7 @@ public final class ActionFactory { map(CronJobURIs.AUTOMATED_FEEDBACK_OPENING_SOON_REMINDERS, GET, FeedbackSessionOpeningSoonRemindersAction.class); map(CronJobURIs.AUTOMATED_USAGE_STATISTICS_COLLECTION, GET, CalculateUsageStatisticsAction.class); + map(CronJobURIs.AUTOMATED_FEEDBACK_SESSION_LOGS_PROCESSING, GET, UpdateFeedbackSessionLogsAction.class); // Task queue workers; use POST request // Reference: https://cloud.google.com/tasks/docs/creating-appengine-tasks diff --git a/src/main/java/teammates/ui/webapi/ActionResult.java b/src/main/java/teammates/ui/webapi/ActionResult.java index 7876dc4ab850..b2c664bbc1a5 100644 --- a/src/main/java/teammates/ui/webapi/ActionResult.java +++ b/src/main/java/teammates/ui/webapi/ActionResult.java @@ -2,7 +2,7 @@ import java.io.IOException; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponse; /** * Represents the result of executing an {@link Action}. diff --git a/src/main/java/teammates/ui/webapi/BasicFeedbackSubmissionAction.java b/src/main/java/teammates/ui/webapi/BasicFeedbackSubmissionAction.java index daf43603a91b..7c3b31931d5f 100644 --- a/src/main/java/teammates/ui/webapi/BasicFeedbackSubmissionAction.java +++ b/src/main/java/teammates/ui/webapi/BasicFeedbackSubmissionAction.java @@ -418,6 +418,7 @@ void verifySessionOpenExceptForModeration(FeedbackSession feedbackSession) throw /** * Gets the section of a recipient. */ + @SuppressWarnings("PMD.ImplicitSwitchFallThrough") // false positive Section getRecipientSection( String courseId, FeedbackParticipantType giverType, FeedbackParticipantType recipientType, String recipientIdentifier) { @@ -465,6 +466,7 @@ Section getRecipientSection( /** * Gets the section of a recipient. */ + @SuppressWarnings("PMD.ImplicitSwitchFallThrough") // false positive String getDatastoreRecipientSection( String courseId, FeedbackParticipantType giverType, FeedbackParticipantType recipientType, String recipientIdentifier) { diff --git a/src/main/java/teammates/ui/webapi/BinFeedbackSessionAction.java b/src/main/java/teammates/ui/webapi/BinFeedbackSessionAction.java index f88dd7da312e..a26404b13f7e 100644 --- a/src/main/java/teammates/ui/webapi/BinFeedbackSessionAction.java +++ b/src/main/java/teammates/ui/webapi/BinFeedbackSessionAction.java @@ -43,13 +43,11 @@ public JsonResult execute() { if (isCourseMigrated(courseId)) { try { - sqlLogic.moveFeedbackSessionToRecycleBin(feedbackSessionName, courseId); + FeedbackSession fs = sqlLogic.moveFeedbackSessionToRecycleBin(feedbackSessionName, courseId); + return new JsonResult(new FeedbackSessionData(fs)); } catch (EntityDoesNotExistException e) { throw new EntityNotFoundException(e); } - - FeedbackSession recycleBinFs = sqlLogic.getFeedbackSessionFromRecycleBin(feedbackSessionName, courseId); - return new JsonResult(new FeedbackSessionData(recycleBinFs)); } else { return oldFeedbackSession(courseId, feedbackSessionName); } diff --git a/src/main/java/teammates/ui/webapi/CreateAccountAction.java b/src/main/java/teammates/ui/webapi/CreateAccountAction.java index c536a09cc9c6..3e5ce9d39427 100644 --- a/src/main/java/teammates/ui/webapi/CreateAccountAction.java +++ b/src/main/java/teammates/ui/webapi/CreateAccountAction.java @@ -9,6 +9,7 @@ import org.apache.http.HttpStatus; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.datatransfer.DataBundle; import teammates.common.datatransfer.attributes.InstructorAttributes; import teammates.common.datatransfer.attributes.StudentAttributes; @@ -92,7 +93,7 @@ public JsonResult execute() throws InvalidHttpRequestBodyException, InvalidOpera } try { - setAccountRequestAsRegistered(accountRequest, instructorEmail, instructorInstitution); + setAccountRequestAsRegistered(accountRequest); } catch (EntityDoesNotExistException | InvalidParametersException e) { // EntityDoesNotExistException should not be thrown as existence of account request has been validated before. // InvalidParametersException should not be thrown as there should not be any invalid parameters. @@ -108,13 +109,10 @@ public JsonResult execute() throws InvalidHttpRequestBodyException, InvalidOpera * * @return the updated account request */ - private AccountRequest setAccountRequestAsRegistered(AccountRequest accountRequest, - String instructorEmail, String instructorInstitution) + private AccountRequest setAccountRequestAsRegistered(AccountRequest accountRequest) throws InvalidParametersException, EntityDoesNotExistException { - accountRequest.setEmail(instructorEmail); - accountRequest.setInstitute(instructorInstitution); + accountRequest.setStatus(AccountRequestStatus.REGISTERED); accountRequest.setRegisteredAt(Instant.now()); - sqlLogic.updateAccountRequest(accountRequest); return accountRequest; } diff --git a/src/main/java/teammates/ui/webapi/CreateAccountRequestAction.java b/src/main/java/teammates/ui/webapi/CreateAccountRequestAction.java index f0f214a04f92..f8ba4d571c0e 100644 --- a/src/main/java/teammates/ui/webapi/CreateAccountRequestAction.java +++ b/src/main/java/teammates/ui/webapi/CreateAccountRequestAction.java @@ -1,16 +1,27 @@ package teammates.ui.webapi; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.exception.InvalidParametersException; import teammates.common.util.EmailWrapper; import teammates.storage.sqlentity.AccountRequest; -import teammates.ui.output.JoinLinkData; +import teammates.ui.output.AccountRequestData; import teammates.ui.request.AccountCreateRequest; import teammates.ui.request.InvalidHttpRequestBodyException; /** * Creates a new account request. */ -public class CreateAccountRequestAction extends AdminOnlyAction { +public class CreateAccountRequestAction extends Action { + + @Override + AuthType getMinAuthLevel() { + return AuthType.PUBLIC; + } + + @Override + void checkSpecificAccessControl() throws UnauthorizedAccessException { + // Nothing needs to be done here because anybody should be able to create an account request. + } @Override public boolean isTransactionNeeded() { @@ -25,29 +36,32 @@ public JsonResult execute() String instructorName = createRequest.getInstructorName().trim(); String instructorEmail = createRequest.getInstructorEmail().trim(); String instructorInstitution = createRequest.getInstructorInstitution().trim(); - + String comments = createRequest.getInstructorComments(); + if (comments != null) { + comments = comments.trim(); + } AccountRequest accountRequest; try { - accountRequest = - sqlLogic.createAccountRequestWithTransaction(instructorName, instructorEmail, instructorInstitution); + accountRequest = sqlLogic.createAccountRequestWithTransaction(instructorName, instructorEmail, + instructorInstitution, AccountRequestStatus.PENDING, comments); } catch (InvalidParametersException ipe) { throw new InvalidHttpRequestBodyException(ipe); } - taskQueuer.scheduleAccountRequestForSearchIndexing(instructorEmail, instructorInstitution); + taskQueuer.scheduleAccountRequestForSearchIndexing(accountRequest.getId().toString()); - if (accountRequest.getRegisteredAt() != null) { - throw new InvalidOperationException("Cannot create account request as instructor has already registered."); - } + assert accountRequest != null; - String joinLink = accountRequest.getRegistrationUrl(); - - EmailWrapper email = emailGenerator.generateNewInstructorAccountJoinEmail( - instructorEmail, instructorName, joinLink); - emailSender.sendEmail(email); + if (userInfo == null || !userInfo.isAdmin) { + EmailWrapper adminAlertEmail = sqlEmailGenerator.generateNewAccountRequestAdminAlertEmail(accountRequest); + EmailWrapper userAcknowledgementEmail = sqlEmailGenerator + .generateNewAccountRequestAcknowledgementEmail(accountRequest); + emailSender.sendEmail(adminAlertEmail); + emailSender.sendEmail(userAcknowledgementEmail); + } - JoinLinkData output = new JoinLinkData(joinLink); + AccountRequestData output = new AccountRequestData(accountRequest); return new JsonResult(output); } diff --git a/src/main/java/teammates/ui/webapi/CreateFeedbackSessionLogAction.java b/src/main/java/teammates/ui/webapi/CreateFeedbackSessionLogAction.java index cf77994aa9f1..5ea725da87b0 100644 --- a/src/main/java/teammates/ui/webapi/CreateFeedbackSessionLogAction.java +++ b/src/main/java/teammates/ui/webapi/CreateFeedbackSessionLogAction.java @@ -1,5 +1,7 @@ package teammates.ui.webapi; +import java.util.UUID; + import teammates.common.datatransfer.logs.FeedbackSessionAuditLogDetails; import teammates.common.datatransfer.logs.FeedbackSessionLogType; import teammates.common.util.Const; @@ -8,7 +10,7 @@ /** * Action: creates a feedback session log for the purposes of tracking and auditing. */ -class CreateFeedbackSessionLogAction extends Action { +public class CreateFeedbackSessionLogAction extends Action { private static final Logger log = Logger.getLogger(); @@ -33,10 +35,8 @@ public JsonResult execute() { String courseId = getNonNullRequestParamValue(Const.ParamsNames.COURSE_ID); String fsName = getNonNullRequestParamValue(Const.ParamsNames.FEEDBACK_SESSION_NAME); String studentEmail = getNonNullRequestParamValue(Const.ParamsNames.STUDENT_EMAIL); - // Skip rigorous validations to avoid incurring extra db reads and to keep the endpoint light - - // Necessary to assist local testing. For production usage, this will be a no-op. - logsProcessor.createFeedbackSessionLog(courseId, studentEmail, fsName, fslType); + // Skip rigorous validations to avoid incurring extra db reads and to keep the endpoint + // light FeedbackSessionAuditLogDetails details = new FeedbackSessionAuditLogDetails(); details.setCourseId(courseId); @@ -44,6 +44,20 @@ public JsonResult execute() { details.setStudentEmail(studentEmail); details.setAccessType(fslType); + if (isCourseMigrated(courseId)) { + UUID studentId = getUuidRequestParamValue(Const.ParamsNames.STUDENT_SQL_ID); + UUID fsId = getUuidRequestParamValue(Const.ParamsNames.FEEDBACK_SESSION_ID); + + details.setStudentId(studentId.toString()); + details.setFeedbackSessionId(fsId.toString()); + + // Necessary to assist local testing. For production usage, this will be a no-op. + logsProcessor.createFeedbackSessionLog(courseId, studentId, fsId, fslType); + } else { + // Necessary to assist local testing. For production usage, this will be a no-op. + logsProcessor.createFeedbackSessionLog(courseId, studentEmail, fsName, fslType); + } + log.event("Feedback session audit event: " + fslType, details); return new JsonResult("Successful"); diff --git a/src/main/java/teammates/ui/webapi/DeleteAccountRequestAction.java b/src/main/java/teammates/ui/webapi/DeleteAccountRequestAction.java index fa12bc67d814..ad157b1e5a39 100644 --- a/src/main/java/teammates/ui/webapi/DeleteAccountRequestAction.java +++ b/src/main/java/teammates/ui/webapi/DeleteAccountRequestAction.java @@ -1,5 +1,7 @@ package teammates.ui.webapi; +import java.util.UUID; + import teammates.common.util.Const; import teammates.storage.sqlentity.AccountRequest; @@ -10,17 +12,16 @@ class DeleteAccountRequestAction extends AdminOnlyAction { @Override public JsonResult execute() throws InvalidOperationException { - String email = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_EMAIL); - String institute = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_INSTITUTION); + UUID id = getUuidRequestParamValue(Const.ParamsNames.ACCOUNT_REQUEST_ID); - AccountRequest toDelete = sqlLogic.getAccountRequest(email, institute); + AccountRequest toDelete = sqlLogic.getAccountRequest(id); if (toDelete != null && toDelete.getRegisteredAt() != null) { // instructor is already registered and cannot be deleted throw new InvalidOperationException("Account request of a registered instructor cannot be deleted."); } - sqlLogic.deleteAccountRequest(email, institute); + sqlLogic.deleteAccountRequest(id); return new JsonResult("Account request successfully deleted."); } diff --git a/src/main/java/teammates/ui/webapi/GateKeeper.java b/src/main/java/teammates/ui/webapi/GateKeeper.java index 75b73feefe07..42edf4a5bbd5 100644 --- a/src/main/java/teammates/ui/webapi/GateKeeper.java +++ b/src/main/java/teammates/ui/webapi/GateKeeper.java @@ -161,8 +161,7 @@ void verifyAccessible(InstructorAttributes instructor, CourseAttributes course, } boolean instructorIsAllowedCoursePrivilege = instructor.isAllowedForPrivilege(privilegeName); - boolean instructorIsAllowedSectionPrivilege = - instructor.getSectionsWithPrivilege(privilegeName).size() != 0; + boolean instructorIsAllowedSectionPrivilege = !instructor.getSectionsWithPrivilege(privilegeName).isEmpty(); if (!instructorIsAllowedCoursePrivilege && !instructorIsAllowedSectionPrivilege) { throw new UnauthorizedAccessException("Course [" + course.getId() + "] is not accessible to instructor [" + instructor.getEmail() + "] for privilege [" + privilegeName + "]"); @@ -179,8 +178,7 @@ void verifyAccessible(Instructor instructor, Course course, String privilegeName verifyAccessible(instructor, course); boolean instructorIsAllowedCoursePrivilege = instructor.isAllowedForPrivilege(privilegeName); - boolean instructorIsAllowedSectionPrivilege = - instructor.getSectionsWithPrivilege(privilegeName).size() != 0; + boolean instructorIsAllowedSectionPrivilege = !instructor.getSectionsWithPrivilege(privilegeName).isEmpty(); if (!instructorIsAllowedCoursePrivilege && !instructorIsAllowedSectionPrivilege) { throw new UnauthorizedAccessException("Course [" + course.getId() + "] is not accessible to instructor [" + instructor.getEmail() + "] for privilege [" + privilegeName + "]"); diff --git a/src/main/java/teammates/ui/webapi/GetAccountRequestAction.java b/src/main/java/teammates/ui/webapi/GetAccountRequestAction.java index a3f3b5195a49..2894d06b2540 100644 --- a/src/main/java/teammates/ui/webapi/GetAccountRequestAction.java +++ b/src/main/java/teammates/ui/webapi/GetAccountRequestAction.java @@ -1,5 +1,7 @@ package teammates.ui.webapi; +import java.util.UUID; + import teammates.common.util.Const; import teammates.storage.sqlentity.AccountRequest; import teammates.ui.output.AccountRequestData; @@ -11,14 +13,12 @@ class GetAccountRequestAction extends AdminOnlyAction { @Override public JsonResult execute() { - String email = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_EMAIL); - String institute = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_INSTITUTION); + UUID id = getUuidRequestParamValue(Const.ParamsNames.ACCOUNT_REQUEST_ID); - AccountRequest accountRequest = sqlLogic.getAccountRequest(email, institute); + AccountRequest accountRequest = sqlLogic.getAccountRequest(id); if (accountRequest == null) { - throw new EntityNotFoundException("Account request for email: " - + email + " and institute: " + institute + " not found."); + throw new EntityNotFoundException("Account request with id: " + id.toString() + " does not exist."); } AccountRequestData output = new AccountRequestData(accountRequest); diff --git a/src/main/java/teammates/ui/webapi/GetAccountRequestsAction.java b/src/main/java/teammates/ui/webapi/GetAccountRequestsAction.java new file mode 100644 index 000000000000..7cac4331b985 --- /dev/null +++ b/src/main/java/teammates/ui/webapi/GetAccountRequestsAction.java @@ -0,0 +1,34 @@ +package teammates.ui.webapi; + +import java.util.List; +import java.util.stream.Collectors; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.util.Const; +import teammates.storage.sqlentity.AccountRequest; +import teammates.ui.output.AccountRequestData; +import teammates.ui.output.AccountRequestsData; + +/** + * Action: Gets pending account requests. + */ +public class GetAccountRequestsAction extends AdminOnlyAction { + @Override + public JsonResult execute() { + String accountRequestStatus = getNonNullRequestParamValue(Const.ParamsNames.ACCOUNT_REQUEST_STATUS); + String pending = AccountRequestStatus.PENDING.name(); // 'PENDING' + if (!pending.equalsIgnoreCase(accountRequestStatus)) { + throw new InvalidHttpParameterException("Only 'pending' is allowed for account request status."); + } + + List accountRequests = sqlLogic.getPendingAccountRequests(); + List accountRequestDatas = accountRequests + .stream() + .map(ar -> new AccountRequestData(ar)) + .collect(Collectors.toList()); + + AccountRequestsData output = new AccountRequestsData(); + output.setAccountRequests(accountRequestDatas); + return new JsonResult(output); + } +} diff --git a/src/main/java/teammates/ui/webapi/GetAuthInfoAction.java b/src/main/java/teammates/ui/webapi/GetAuthInfoAction.java index ed2aaa444ea8..28dc4ca030cb 100644 --- a/src/main/java/teammates/ui/webapi/GetAuthInfoAction.java +++ b/src/main/java/teammates/ui/webapi/GetAuthInfoAction.java @@ -3,7 +3,7 @@ import java.util.Collections; import java.util.List; -import javax.servlet.http.Cookie; +import jakarta.servlet.http.Cookie; import teammates.common.util.Const; import teammates.common.util.HttpRequestHelper; diff --git a/src/main/java/teammates/ui/webapi/GetCoursesAction.java b/src/main/java/teammates/ui/webapi/GetCoursesAction.java index 41d1cf55c6dc..76e8963e3c16 100644 --- a/src/main/java/teammates/ui/webapi/GetCoursesAction.java +++ b/src/main/java/teammates/ui/webapi/GetCoursesAction.java @@ -34,8 +34,8 @@ AuthType getMinAuthLevel() { void checkSpecificAccessControl() throws UnauthorizedAccessException { String entityType = getNonNullRequestParamValue(Const.ParamsNames.ENTITY_TYPE); - if (!(entityType.equals(Const.EntityType.STUDENT) && userInfo.isStudent) - && !(entityType.equals(Const.EntityType.INSTRUCTOR) && userInfo.isInstructor)) { + if (!(Const.EntityType.STUDENT.equals(entityType) && userInfo.isStudent) + && !(Const.EntityType.INSTRUCTOR.equals(entityType) && userInfo.isInstructor)) { throw new UnauthorizedAccessException("Current account cannot access to courses of request entity type"); } } diff --git a/src/main/java/teammates/ui/webapi/GetFeedbackSessionLogsAction.java b/src/main/java/teammates/ui/webapi/GetFeedbackSessionLogsAction.java index 72332d439e5b..720ec6e726bc 100644 --- a/src/main/java/teammates/ui/webapi/GetFeedbackSessionLogsAction.java +++ b/src/main/java/teammates/ui/webapi/GetFeedbackSessionLogsAction.java @@ -1,10 +1,12 @@ package teammates.ui.webapi; +import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; import teammates.common.datatransfer.FeedbackSessionLogEntry; @@ -17,6 +19,7 @@ import teammates.common.util.TimeHelper; import teammates.storage.sqlentity.Course; import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; import teammates.storage.sqlentity.Instructor; import teammates.storage.sqlentity.Student; import teammates.ui.output.FeedbackSessionLogsData; @@ -65,36 +68,6 @@ void checkSpecificAccessControl() throws UnauthorizedAccessException { @Override public JsonResult execute() { - String courseId = getNonNullRequestParamValue(Const.ParamsNames.COURSE_ID); - String email = getRequestParamValue(Const.ParamsNames.STUDENT_EMAIL); - String feedbackSessionName = getRequestParamValue(Const.ParamsNames.FEEDBACK_SESSION_NAME); - - if (isCourseMigrated(courseId)) { - if (sqlLogic.getCourse(courseId) == null) { - throw new EntityNotFoundException("Course not found"); - } - - if (email != null && sqlLogic.getStudentForEmail(courseId, email) == null) { - throw new EntityNotFoundException("Student not found"); - } - - if (feedbackSessionName != null && sqlLogic.getFeedbackSession(feedbackSessionName, courseId) == null) { - throw new EntityNotFoundException("Feedback session not found"); - } - } else { - if (logic.getCourse(courseId) == null) { - throw new EntityNotFoundException("Course not found"); - } - - if (email != null && logic.getStudentForEmail(courseId, email) == null) { - throw new EntityNotFoundException("Student not found"); - } - - if (feedbackSessionName != null && logic.getFeedbackSession(feedbackSessionName, courseId) == null) { - throw new EntityNotFoundException("Feedback session not found"); - } - } - String fslTypes = getRequestParamValue(Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE); List convertedFslTypes = new ArrayList<>(); if (fslTypes != null) { @@ -126,51 +99,92 @@ public JsonResult execute() { throw new InvalidHttpParameterException("The end time should be after the start time."); } - long earliestSearchTime = TimeHelper.getInstantDaysOffsetBeforeNow(Const.LOGS_RETENTION_PERIOD.toDays()) - .toEpochMilli(); - if (startTime < earliestSearchTime) { - throw new InvalidHttpParameterException( - "The earliest date you can search for is " + Const.LOGS_RETENTION_PERIOD.toDays() + " days before today." - ); - } + String courseId = getNonNullRequestParamValue(Const.ParamsNames.COURSE_ID); - List fsLogEntries = - logsProcessor.getFeedbackSessionLogs(courseId, email, startTime, endTime, feedbackSessionName); + if (!isCourseMigrated(courseId)) { + long earliestSearchTime = TimeHelper.getInstantDaysOffsetBeforeNow(Const.LOGS_RETENTION_PERIOD.toDays()) + .toEpochMilli(); + if (startTime < earliestSearchTime) { + throw new InvalidHttpParameterException("The earliest date you can search for is " + + Const.LOGS_RETENTION_PERIOD.toDays() + " days before today."); + } + } if (isCourseMigrated(courseId)) { + UUID studentId = null; + UUID feedbackSessionId = null; + String studentIdString = getRequestParamValue(Const.ParamsNames.STUDENT_SQL_ID); + String feedbackSessionIdString = getRequestParamValue(Const.ParamsNames.FEEDBACK_SESSION_ID); + + if (studentIdString != null) { + studentId = getUuidFromString(Const.ParamsNames.STUDENT_SQL_ID, studentIdString); + } + + if (feedbackSessionIdString != null) { + feedbackSessionId = getUuidFromString(Const.ParamsNames.FEEDBACK_SESSION_ID, feedbackSessionIdString); + } + + if (sqlLogic.getCourse(courseId) == null) { + throw new EntityNotFoundException("Course not found"); + } + + if (studentId != null && sqlLogic.getStudent(studentId) == null) { + throw new EntityNotFoundException("Student not found"); + } + + if (feedbackSessionId != null && sqlLogic.getFeedbackSession(feedbackSessionId) == null) { + throw new EntityNotFoundException("Feedback session not found"); + } + + List fsLogEntries = sqlLogic.getOrderedFeedbackSessionLogs(courseId, studentId, + feedbackSessionId, Instant.ofEpochMilli(startTime), Instant.ofEpochMilli(endTime)); Map studentsMap = new HashMap<>(); Map sessionsMap = new HashMap<>(); List feedbackSessions = sqlLogic.getFeedbackSessionsForCourse(courseId); feedbackSessions.forEach(fs -> sessionsMap.put(fs.getName(), fs)); fsLogEntries = fsLogEntries.stream().filter(logEntry -> { - String logType = logEntry.getFeedbackSessionLogType(); - FeedbackSessionLogType convertedLogType = FeedbackSessionLogType.valueOfLabel(logType); - if (convertedLogType == null || fslTypes != null && !convertedFslTypes.contains(convertedLogType)) { + FeedbackSessionLogType logType = logEntry.getFeedbackSessionLogType(); + if (logType == null || fslTypes != null && !convertedFslTypes.contains(logType)) { // If the feedback session log type retrieved from the log is invalid // or not the type being queried, ignore the log return false; } - if (!studentsMap.containsKey(logEntry.getStudentEmail())) { - Student student = sqlLogic.getStudentForEmail(courseId, logEntry.getStudentEmail()); + if (!studentsMap.containsKey(logEntry.getStudent().getEmail())) { + Student student = sqlLogic.getStudent(logEntry.getStudent().getId()); if (student == null) { // If the student email retrieved from the log is invalid, ignore the log return false; } - studentsMap.put(logEntry.getStudentEmail(), student); + studentsMap.put(student.getEmail(), student); } // If the feedback session retrieved from the log is invalid, ignore the log - return sessionsMap.containsKey(logEntry.getFeedbackSessionName()); + return sessionsMap.containsKey(logEntry.getFeedbackSession().getName()); }).collect(Collectors.toList()); - Map> groupedEntries = - groupFeedbackSessionLogEntries(fsLogEntries); + Map> groupedEntries = groupFeedbackSessionLogs(fsLogEntries); feedbackSessions.forEach(fs -> groupedEntries.putIfAbsent(fs.getName(), new ArrayList<>())); FeedbackSessionLogsData fslData = new FeedbackSessionLogsData(groupedEntries, studentsMap, sessionsMap); return new JsonResult(fslData); } else { + if (logic.getCourse(courseId) == null) { + throw new EntityNotFoundException("Course not found"); + } + + String email = getRequestParamValue(Const.ParamsNames.STUDENT_EMAIL); + if (email != null && logic.getStudentForEmail(courseId, email) == null) { + throw new EntityNotFoundException("Student not found"); + } + + String feedbackSessionName = getRequestParamValue(Const.ParamsNames.FEEDBACK_SESSION_NAME); + if (feedbackSessionName != null && logic.getFeedbackSession(feedbackSessionName, courseId) == null) { + throw new EntityNotFoundException("Feedback session not found"); + } + + List fsLogEntries = + logsProcessor.getOrderedFeedbackSessionLogs(courseId, email, startTime, endTime, feedbackSessionName); Map studentsMap = new HashMap<>(); Map sessionsMap = new HashMap<>(); List feedbackSessions = logic.getFeedbackSessionsForCourse(courseId); @@ -215,4 +229,14 @@ private Map> groupFeedbackSessionLogEntrie } return groupedEntries; } + + private Map> groupFeedbackSessionLogs( + List fsLogEntries) { + Map> groupedEntries = new LinkedHashMap<>(); + for (FeedbackSessionLog fsLogEntry : fsLogEntries) { + String fsName = fsLogEntry.getFeedbackSession().getName(); + groupedEntries.computeIfAbsent(fsName, k -> new ArrayList<>()).add(fsLogEntry); + } + return groupedEntries; + } } diff --git a/src/main/java/teammates/ui/webapi/GetFeedbackSessionsAction.java b/src/main/java/teammates/ui/webapi/GetFeedbackSessionsAction.java index b14a5369bd84..d4c1c1ae9549 100644 --- a/src/main/java/teammates/ui/webapi/GetFeedbackSessionsAction.java +++ b/src/main/java/teammates/ui/webapi/GetFeedbackSessionsAction.java @@ -38,13 +38,13 @@ void checkSpecificAccessControl() throws UnauthorizedAccessException { String entityType = getNonNullRequestParamValue(Const.ParamsNames.ENTITY_TYPE); - if (!(entityType.equals(Const.EntityType.STUDENT) || entityType.equals(Const.EntityType.INSTRUCTOR))) { + if (!(Const.EntityType.STUDENT.equals(entityType) || Const.EntityType.INSTRUCTOR.equals(entityType))) { throw new UnauthorizedAccessException("entity type not supported."); } String courseId = getRequestParamValue(Const.ParamsNames.COURSE_ID); - if (entityType.equals(Const.EntityType.STUDENT)) { + if (Const.EntityType.STUDENT.equals(entityType)) { if (!userInfo.isStudent) { throw new UnauthorizedAccessException("User " + userInfo.getId() + " does not have student privileges"); @@ -90,7 +90,7 @@ public JsonResult execute() { List studentEmails = new ArrayList<>(); if (courseId == null) { - if (entityType.equals(Const.EntityType.STUDENT)) { + if (Const.EntityType.STUDENT.equals(entityType)) { List students = sqlLogic.getStudentsByGoogleId(userInfo.getId()); for (Student student : students) { String studentCourseId = student.getCourse().getId(); @@ -113,7 +113,7 @@ public JsonResult execute() { feedbackSessionAttributes.addAll(sessions); } - } else if (entityType.equals(Const.EntityType.INSTRUCTOR)) { + } else if (Const.EntityType.INSTRUCTOR.equals(entityType)) { boolean isInRecycleBin = getBooleanRequestParamValue(Const.ParamsNames.IS_IN_RECYCLE_BIN); instructors = sqlLogic.getInstructorsForGoogleId(userInfo.getId()); @@ -135,33 +135,33 @@ public JsonResult execute() { } else { if (isCourseMigrated(courseId)) { feedbackSessions = sqlLogic.getFeedbackSessionsForCourse(courseId); - if (entityType.equals(Const.EntityType.STUDENT) && !feedbackSessions.isEmpty()) { + if (Const.EntityType.STUDENT.equals(entityType) && !feedbackSessions.isEmpty()) { Student student = sqlLogic.getStudentByGoogleId(courseId, userInfo.getId()); assert student != null; String emailAddress = student.getEmail(); studentEmails.add(emailAddress); - } else if (entityType.equals(Const.EntityType.INSTRUCTOR)) { + } else if (Const.EntityType.INSTRUCTOR.equals(entityType)) { instructors = Collections.singletonList( sqlLogic.getInstructorByGoogleId(courseId, userInfo.getId())); } } else { feedbackSessionAttributes = logic.getFeedbackSessionsForCourse(courseId); - if (entityType.equals(Const.EntityType.STUDENT) && !feedbackSessionAttributes.isEmpty()) { + if (Const.EntityType.STUDENT.equals(entityType) && !feedbackSessionAttributes.isEmpty()) { StudentAttributes student = logic.getStudentForGoogleId(courseId, userInfo.getId()); assert student != null; String emailAddress = student.getEmail(); feedbackSessionAttributes = feedbackSessionAttributes.stream() .map(instructorSession -> instructorSession.getCopyForStudent(emailAddress)) .collect(Collectors.toList()); - } else if (entityType.equals(Const.EntityType.INSTRUCTOR)) { + } else if (Const.EntityType.INSTRUCTOR.equals(entityType)) { dataStoreInstructors = Collections.singletonList(logic.getInstructorForGoogleId(courseId, userInfo.getId())); } } } - if (entityType.equals(Const.EntityType.STUDENT)) { + if (Const.EntityType.STUDENT.equals(entityType)) { // hide session not visible to student feedbackSessions = feedbackSessions.stream() .filter(FeedbackSession::isVisible).collect(Collectors.toList()); @@ -182,9 +182,9 @@ public JsonResult execute() { responseData.hideInformationForStudent(studentEmail); } - if (entityType.equals(Const.EntityType.STUDENT)) { + if (Const.EntityType.STUDENT.equals(entityType)) { responseData.getFeedbackSessions().forEach(FeedbackSessionData::hideInformationForStudent); - } else if (entityType.equals(Const.EntityType.INSTRUCTOR)) { + } else if (Const.EntityType.INSTRUCTOR.equals(entityType)) { responseData.getFeedbackSessions().forEach(session -> { Instructor instructor = courseIdToInstructor.get(session.getCourseId()); InstructorAttributes dataStoreInstructor = dataStoreCourseIdToInstructor.get(session.getCourseId()); diff --git a/src/main/java/teammates/ui/webapi/GetHasResponsesAction.java b/src/main/java/teammates/ui/webapi/GetHasResponsesAction.java index f5a2ecb57c63..668d5e3a84ef 100644 --- a/src/main/java/teammates/ui/webapi/GetHasResponsesAction.java +++ b/src/main/java/teammates/ui/webapi/GetHasResponsesAction.java @@ -30,11 +30,11 @@ void checkSpecificAccessControl() throws UnauthorizedAccessException { String entityType = getNonNullRequestParamValue(Const.ParamsNames.ENTITY_TYPE); - if (!(entityType.equals(Const.EntityType.STUDENT) || entityType.equals(Const.EntityType.INSTRUCTOR))) { + if (!(Const.EntityType.STUDENT.equals(entityType) || Const.EntityType.INSTRUCTOR.equals(entityType))) { throw new UnauthorizedAccessException("entity type not supported."); } - if (entityType.equals(Const.EntityType.INSTRUCTOR)) { + if (Const.EntityType.INSTRUCTOR.equals(entityType)) { //An instructor of the feedback session can check responses for questions within it. String questionId = getRequestParamValue(Const.ParamsNames.FEEDBACK_QUESTION_ID); if (questionId != null) { @@ -117,7 +117,7 @@ void checkSpecificAccessControl() throws UnauthorizedAccessException { public JsonResult execute() { String entityType = getNonNullRequestParamValue(Const.ParamsNames.ENTITY_TYPE); - if (entityType.equals(Const.EntityType.INSTRUCTOR)) { + if (Const.EntityType.INSTRUCTOR.equals(entityType)) { return handleInstructorReq(); } diff --git a/src/main/java/teammates/ui/webapi/GetSessionResultsAction.java b/src/main/java/teammates/ui/webapi/GetSessionResultsAction.java index 5de4fce08e3b..d1aa7d9f09b5 100644 --- a/src/main/java/teammates/ui/webapi/GetSessionResultsAction.java +++ b/src/main/java/teammates/ui/webapi/GetSessionResultsAction.java @@ -13,6 +13,7 @@ import teammates.storage.sqlentity.FeedbackSession; import teammates.storage.sqlentity.Instructor; import teammates.storage.sqlentity.Student; +import teammates.storage.sqlentity.Team; import teammates.ui.output.SessionResultsData; import teammates.ui.request.Intent; @@ -126,7 +127,7 @@ public JsonResult execute() { if (isCourseMigrated(courseId)) { if (questionId != null) { UUID questionUuid = getUuidRequestParamValue(Const.ParamsNames.FEEDBACK_QUESTION_ID); - executeWithSql(courseId, feedbackSessionName, questionUuid, + return executeWithSql(courseId, feedbackSessionName, questionUuid, selectedSection, fetchType, intent, isPreviewResults); } return executeWithSql(courseId, feedbackSessionName, null, selectedSection, @@ -195,14 +196,14 @@ private JsonResult executeWithSql( return new JsonResult(SessionResultsData.initForInstructor(bundle)); case INSTRUCTOR_RESULT: // Section name filter is not applicable here - instructor = getPossiblyUnregisteredSqlInstructor(courseId); + instructor = getSqlInstructorOfCourseFromRequest(courseId); bundle = sqlLogic.getSessionResultsForUser(feedbackSession, courseId, instructor.getEmail(), true, questionUuid, isPreviewResults); // Build a fake student object, as the results will be displayed as if they are displayed to a student student = new Student(instructor.getCourse(), instructor.getName(), instructor.getEmail(), ""); - student.setTeam(instructor.getTeam()); + student.setTeam(new Team(null, Const.USER_TEAM_FOR_INSTRUCTOR)); return new JsonResult(SessionResultsData.initForStudent(bundle, student)); case STUDENT_RESULT: diff --git a/src/main/java/teammates/ui/webapi/GetStudentsAction.java b/src/main/java/teammates/ui/webapi/GetStudentsAction.java index e422aa16e931..1dd1c7a1cfca 100644 --- a/src/main/java/teammates/ui/webapi/GetStudentsAction.java +++ b/src/main/java/teammates/ui/webapi/GetStudentsAction.java @@ -68,7 +68,7 @@ public JsonResult execute() { boolean hasCoursePrivilege = instructor != null && instructor.isAllowedForPrivilege(privilegeName); boolean hasSectionPrivilege = instructor != null - && instructor.getSectionsWithPrivilege(privilegeName).size() != 0; + && !instructor.getSectionsWithPrivilege(privilegeName).isEmpty(); if (teamName == null && hasCoursePrivilege) { // request to get all course students by instructor with course privilege @@ -104,7 +104,7 @@ public JsonResult execute() { boolean hasCoursePrivilege = instructor != null && instructor.isAllowedForPrivilege(privilegeName); boolean hasSectionPrivilege = instructor != null - && instructor.getSectionsWithPrivilege(privilegeName).size() != 0; + && !instructor.getSectionsWithPrivilege(privilegeName).isEmpty(); if (teamName == null && hasCoursePrivilege) { // request to get all course students by instructor with course privilege diff --git a/src/main/java/teammates/ui/webapi/JsonResult.java b/src/main/java/teammates/ui/webapi/JsonResult.java index db9460311f61..b966be65f9f0 100644 --- a/src/main/java/teammates/ui/webapi/JsonResult.java +++ b/src/main/java/teammates/ui/webapi/JsonResult.java @@ -5,8 +5,8 @@ import java.util.ArrayList; import java.util.List; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus; diff --git a/src/main/java/teammates/ui/webapi/RejectAccountRequestAction.java b/src/main/java/teammates/ui/webapi/RejectAccountRequestAction.java new file mode 100644 index 000000000000..f34d6ea3467f --- /dev/null +++ b/src/main/java/teammates/ui/webapi/RejectAccountRequestAction.java @@ -0,0 +1,59 @@ +package teammates.ui.webapi; + +import java.util.UUID; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.exception.EntityDoesNotExistException; +import teammates.common.exception.InvalidParametersException; +import teammates.common.util.Const; +import teammates.common.util.EmailWrapper; +import teammates.storage.sqlentity.AccountRequest; +import teammates.ui.output.AccountRequestData; +import teammates.ui.request.AccountRequestRejectionRequest; +import teammates.ui.request.InvalidHttpRequestBodyException; + +/** + * Rejects an account request. + */ +public class RejectAccountRequestAction extends AdminOnlyAction { + + @Override + public boolean isTransactionNeeded() { + return false; + } + + @Override + public JsonResult execute() throws InvalidOperationException, InvalidHttpRequestBodyException { + String id = getNonNullRequestParamValue(Const.ParamsNames.ACCOUNT_REQUEST_ID); + UUID accountRequestId = getUuidFromString(Const.ParamsNames.ACCOUNT_REQUEST_ID, id); + + AccountRequest accountRequest = sqlLogic.getAccountRequestWithTransaction(accountRequestId); + + if (accountRequest == null) { + String errorMessage = String.format(Const.ACCOUNT_REQUEST_NOT_FOUND, accountRequestId.toString()); + throw new EntityNotFoundException(errorMessage); + } + + AccountRequestRejectionRequest accountRequestRejectionRequest = + getAndValidateRequestBody(AccountRequestRejectionRequest.class); + AccountRequestStatus initialStatus = accountRequest.getStatus(); + + try { + accountRequest.setStatus(AccountRequestStatus.REJECTED); + accountRequest = sqlLogic.updateAccountRequestWithTransaction(accountRequest); + if (accountRequestRejectionRequest.checkHasReason() + && initialStatus != AccountRequestStatus.REJECTED) { + EmailWrapper email = sqlEmailGenerator.generateAccountRequestRejectionEmail(accountRequest, + accountRequestRejectionRequest.getReasonTitle(), accountRequestRejectionRequest.getReasonBody()); + emailSender.sendEmail(email); + } + taskQueuer.scheduleAccountRequestForSearchIndexing(accountRequest.getId().toString()); + } catch (InvalidParametersException e) { + throw new InvalidHttpRequestBodyException(e); + } catch (EntityDoesNotExistException e) { + throw new EntityNotFoundException(e); + } + + return new JsonResult(new AccountRequestData(accountRequest)); + } +} diff --git a/src/main/java/teammates/ui/webapi/ResetAccountRequestAction.java b/src/main/java/teammates/ui/webapi/ResetAccountRequestAction.java index 7fcd3a40c6bc..f0ff7ff86b3f 100644 --- a/src/main/java/teammates/ui/webapi/ResetAccountRequestAction.java +++ b/src/main/java/teammates/ui/webapi/ResetAccountRequestAction.java @@ -1,5 +1,7 @@ package teammates.ui.webapi; +import java.util.UUID; + import org.apache.http.HttpStatus; import teammates.common.exception.EntityDoesNotExistException; @@ -19,21 +21,19 @@ class ResetAccountRequestAction extends AdminOnlyAction { @Override public JsonResult execute() throws InvalidOperationException { - String instructorEmail = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_EMAIL); - String institute = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_INSTITUTION); + UUID id = getUuidRequestParamValue(Const.ParamsNames.ACCOUNT_REQUEST_ID); - AccountRequest accountRequest = sqlLogic.getAccountRequest(instructorEmail, institute); + AccountRequest accountRequest = sqlLogic.getAccountRequest(id); if (accountRequest == null) { - throw new EntityNotFoundException("Account request for instructor with email: " + instructorEmail - + " and institute: " + institute + " does not exist."); + throw new EntityNotFoundException("Account request with id: " + id.toString() + " does not exist."); } if (accountRequest.getRegisteredAt() == null) { throw new InvalidOperationException("Unable to reset account request as instructor is still unregistered."); } try { - accountRequest = sqlLogic.resetAccountRequest(instructorEmail, institute); + accountRequest = sqlLogic.resetAccountRequest(id); } catch (InvalidParametersException | EntityDoesNotExistException ue) { // InvalidParametersException and EntityDoesNotExistException should not be thrown as // validity of params has been verified when fetching entity. diff --git a/src/main/java/teammates/ui/webapi/RestoreFeedbackSessionAction.java b/src/main/java/teammates/ui/webapi/RestoreFeedbackSessionAction.java index c93d6ba4b2c8..33a19137d32c 100644 --- a/src/main/java/teammates/ui/webapi/RestoreFeedbackSessionAction.java +++ b/src/main/java/teammates/ui/webapi/RestoreFeedbackSessionAction.java @@ -23,12 +23,23 @@ AuthType getMinAuthLevel() { void checkSpecificAccessControl() throws UnauthorizedAccessException { String courseId = getNonNullRequestParamValue(Const.ParamsNames.COURSE_ID); String feedbackSessionName = getNonNullRequestParamValue(Const.ParamsNames.FEEDBACK_SESSION_NAME); - FeedbackSessionAttributes feedbackSession = logic.getFeedbackSessionFromRecycleBin(feedbackSessionName, courseId); - gateKeeper.verifyAccessible( - logic.getInstructorForGoogleId(courseId, userInfo.getId()), - feedbackSession, - Const.InstructorPermissions.CAN_MODIFY_SESSION); + if (isCourseMigrated(courseId)) { + FeedbackSession feedbackSession = sqlLogic.getFeedbackSessionFromRecycleBin(feedbackSessionName, courseId); + + gateKeeper.verifyAccessible( + sqlLogic.getInstructorByGoogleId(courseId, userInfo.getId()), + feedbackSession, + Const.InstructorPermissions.CAN_MODIFY_SESSION); + } else { + FeedbackSessionAttributes feedbackSession = + logic.getFeedbackSessionFromRecycleBin(feedbackSessionName, courseId); + + gateKeeper.verifyAccessible( + logic.getInstructorForGoogleId(courseId, userInfo.getId()), + feedbackSession, + Const.InstructorPermissions.CAN_MODIFY_SESSION); + } } @Override diff --git a/src/main/java/teammates/ui/webapi/SearchStudentsAction.java b/src/main/java/teammates/ui/webapi/SearchStudentsAction.java index c975dae87567..d801f7e48d1b 100644 --- a/src/main/java/teammates/ui/webapi/SearchStudentsAction.java +++ b/src/main/java/teammates/ui/webapi/SearchStudentsAction.java @@ -38,10 +38,10 @@ public JsonResult execute() { List students; try { - if (userInfo.isInstructor && entity.equals(Const.EntityType.INSTRUCTOR)) { + if (userInfo.isInstructor && Const.EntityType.INSTRUCTOR.equals(entity)) { List instructors = sqlLogic.getInstructorsForGoogleId(userInfo.id); students = sqlLogic.searchStudents(searchKey, instructors); - } else if (userInfo.isAdmin && entity.equals(Const.EntityType.ADMIN)) { + } else if (userInfo.isAdmin && Const.EntityType.ADMIN.equals(entity)) { students = sqlLogic.searchStudentsInWholeSystem(searchKey); } else { throw new InvalidHttpParameterException("Invalid entity type for search"); @@ -53,10 +53,10 @@ public JsonResult execute() { // Search in datastore. For more information on dual db support, see this [PR](https://github.com/TEAMMATES/teammates/pull/12728/files) List studentsDatastore; try { - if (userInfo.isInstructor && entity.equals(Const.EntityType.INSTRUCTOR)) { + if (userInfo.isInstructor && Const.EntityType.INSTRUCTOR.equals(entity)) { List instructors = logic.getInstructorsForGoogleId(userInfo.id); studentsDatastore = logic.searchStudents(searchKey, instructors); - } else if (userInfo.isAdmin && entity.equals(Const.EntityType.ADMIN)) { + } else if (userInfo.isAdmin && Const.EntityType.ADMIN.equals(entity)) { studentsDatastore = logic.searchStudentsInWholeSystem(searchKey); } else { throw new InvalidHttpParameterException("Invalid entity type for search"); @@ -70,7 +70,7 @@ public JsonResult execute() { for (Student s : students) { StudentData studentData = new StudentData(s); - if (userInfo.isAdmin && entity.equals(Const.EntityType.ADMIN)) { + if (userInfo.isAdmin && Const.EntityType.ADMIN.equals(entity)) { studentData.addAdditionalInformationForAdminSearch( s.getRegKey(), sqlLogic.getCourseInstitute(s.getCourseId()), @@ -88,7 +88,7 @@ public JsonResult execute() { continue; } - if (userInfo.isAdmin && entity.equals(Const.EntityType.ADMIN)) { + if (userInfo.isAdmin && Const.EntityType.ADMIN.equals(entity)) { studentData.addAdditionalInformationForAdminSearch( s.getKey(), logic.getCourseInstitute(s.getCourse()), diff --git a/src/main/java/teammates/ui/webapi/UpdateAccountRequestAction.java b/src/main/java/teammates/ui/webapi/UpdateAccountRequestAction.java new file mode 100644 index 000000000000..c3e756c8b4df --- /dev/null +++ b/src/main/java/teammates/ui/webapi/UpdateAccountRequestAction.java @@ -0,0 +1,88 @@ +package teammates.ui.webapi; + +import java.util.UUID; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.exception.EntityDoesNotExistException; +import teammates.common.exception.InvalidParametersException; +import teammates.common.util.Const; +import teammates.common.util.EmailWrapper; +import teammates.storage.sqlentity.AccountRequest; +import teammates.ui.output.AccountRequestData; +import teammates.ui.request.AccountRequestUpdateRequest; +import teammates.ui.request.InvalidHttpRequestBodyException; + +/** + * Updates an account request. + */ +public class UpdateAccountRequestAction extends AdminOnlyAction { + + @Override + public boolean isTransactionNeeded() { + return false; + } + + @Override + public JsonResult execute() throws InvalidOperationException, InvalidHttpRequestBodyException { + String id = getNonNullRequestParamValue(Const.ParamsNames.ACCOUNT_REQUEST_ID); + UUID accountRequestId = getUuidFromString(Const.ParamsNames.ACCOUNT_REQUEST_ID, id); + + AccountRequest accountRequest = sqlLogic.getAccountRequestWithTransaction(accountRequestId); + + if (accountRequest == null) { + String errorMessage = String.format(Const.ACCOUNT_REQUEST_NOT_FOUND, accountRequestId.toString()); + throw new EntityNotFoundException(errorMessage); + } + + AccountRequestUpdateRequest accountRequestUpdateRequest = + getAndValidateRequestBody(AccountRequestUpdateRequest.class); + + if (accountRequestUpdateRequest.getStatus() == AccountRequestStatus.APPROVED + && (accountRequest.getStatus() == AccountRequestStatus.PENDING + || accountRequest.getStatus() == AccountRequestStatus.REJECTED)) { + + if (!sqlLogic.getAccountsForEmailWithTransaction(accountRequest.getEmail()).isEmpty()) { + throw new InvalidOperationException(String.format("An account with email %s already exists. " + + "Please reject or delete the account request instead.", + accountRequest.getEmail())); + } + + if (!sqlLogic.getApprovedAccountRequestsForEmailWithTransaction(accountRequest.getEmail()).isEmpty()) { + throw new InvalidOperationException(String.format( + "An account request with email %s has already been approved. " + + "Please reject or delete the account request instead.", + accountRequest.getEmail())); + } + + try { + // should not need to update other fields for an approval + accountRequest.setStatus(accountRequestUpdateRequest.getStatus()); + accountRequest = sqlLogic.updateAccountRequestWithTransaction(accountRequest); + EmailWrapper email = sqlEmailGenerator.generateNewInstructorAccountJoinEmail( + accountRequest.getEmail(), accountRequest.getName(), accountRequest.getRegistrationUrl()); + taskQueuer.scheduleAccountRequestForSearchIndexing(accountRequest.getId().toString()); + emailSender.sendEmail(email); + } catch (InvalidParametersException e) { + throw new InvalidHttpRequestBodyException(e); + } catch (EntityDoesNotExistException e) { + throw new EntityNotFoundException(e); + } + } else { + try { + accountRequest.setName(accountRequestUpdateRequest.getName()); + accountRequest.setEmail(accountRequestUpdateRequest.getEmail()); + accountRequest.setInstitute(accountRequestUpdateRequest.getInstitute()); + accountRequest.setStatus(accountRequest.getStatus()); + accountRequest.setComments(accountRequestUpdateRequest.getComments()); + accountRequest = sqlLogic.updateAccountRequestWithTransaction(accountRequest); + taskQueuer.scheduleAccountRequestForSearchIndexing(accountRequest.getId().toString()); + } catch (InvalidParametersException e) { + throw new InvalidHttpRequestBodyException(e); + } catch (EntityDoesNotExistException e) { + throw new EntityNotFoundException(e); + } + } + + return new JsonResult(new AccountRequestData(accountRequest)); + } +} diff --git a/src/main/java/teammates/ui/webapi/UpdateFeedbackSessionLogsAction.java b/src/main/java/teammates/ui/webapi/UpdateFeedbackSessionLogsAction.java new file mode 100644 index 000000000000..9324d4615dfd --- /dev/null +++ b/src/main/java/teammates/ui/webapi/UpdateFeedbackSessionLogsAction.java @@ -0,0 +1,77 @@ +package teammates.ui.webapi; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import teammates.common.datatransfer.FeedbackSessionLogEntry; +import teammates.common.datatransfer.attributes.CourseAttributes; +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.util.Const; +import teammates.common.util.TimeHelper; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.storage.sqlentity.Student; + +/** + * Process feedback session logs from GCP in the past defined time period and + * store in the database. + */ +public class UpdateFeedbackSessionLogsAction extends AdminOnlyAction { + + static final long COLLECTION_TIME_PERIOD = Const.STUDENT_ACTIVITY_LOGS_UPDATE_INTERVAL.toMinutes(); + static final long SPAM_FILTER = Const.STUDENT_ACTIVITY_LOGS_FILTER_WINDOW.toMillis(); + + @Override + public JsonResult execute() { + List filteredLogs = new ArrayList<>(); + + Instant endTime = TimeHelper.getInstantNearestQuarterHourBefore(Instant.now()); + Instant startTime = endTime.minus(COLLECTION_TIME_PERIOD, ChronoUnit.MINUTES); + + List logEntries = logsProcessor.getOrderedFeedbackSessionLogs(null, null, + startTime.toEpochMilli(), endTime.toEpochMilli(), null); + + Map>>> lastSavedTimestamps = new HashMap<>(); + Map isCourseMigratedMap = new HashMap<>(); + for (FeedbackSessionLogEntry logEntry : logEntries) { + + isCourseMigratedMap.computeIfAbsent(logEntry.getCourseId(), k -> { + CourseAttributes course = logic.getCourse(logEntry.getCourseId()); + return course == null || course.isMigrated(); + }); + + if (!isCourseMigratedMap.get(logEntry.getCourseId())) { + continue; + } + + String courseId = logEntry.getCourseId(); + UUID studentId = logEntry.getStudentId(); + UUID fbSessionId = logEntry.getFeedbackSessionId(); + String type = logEntry.getFeedbackSessionLogType(); + Long timestamp = logEntry.getTimestamp(); + + lastSavedTimestamps.computeIfAbsent(studentId, k -> new HashMap<>()); + lastSavedTimestamps.get(studentId).computeIfAbsent(courseId, k -> new HashMap<>()); + lastSavedTimestamps.get(studentId).get(courseId).computeIfAbsent(fbSessionId, k -> new HashMap<>()); + Long lastSaved = lastSavedTimestamps.get(studentId).get(courseId).get(fbSessionId).getOrDefault(type, 0L); + + if (Math.abs(timestamp - lastSaved) > SPAM_FILTER) { + lastSavedTimestamps.get(studentId).get(courseId).get(fbSessionId).put(type, timestamp); + Student student = sqlLogic.getStudentReference(studentId); + FeedbackSession feedbackSession = sqlLogic.getFeedbackSessionReference(fbSessionId); + FeedbackSessionLog fslEntity = new FeedbackSessionLog(student, feedbackSession, + FeedbackSessionLogType.valueOfLabel(type), Instant.ofEpochMilli(timestamp)); + filteredLogs.add(fslEntity); + } + } + + sqlLogic.createFeedbackSessionLogs(filteredLogs); + + return new JsonResult("Successful"); + } +} diff --git a/src/main/resources/adminEmailTemplate-newAccountRequestAlert.html b/src/main/resources/adminEmailTemplate-newAccountRequestAlert.html new file mode 100644 index 000000000000..a91c1250a264 --- /dev/null +++ b/src/main/resources/adminEmailTemplate-newAccountRequestAlert.html @@ -0,0 +1,60 @@ +

    Hello, Admin

    + +

    + A new instructor account request has been submitted: +

    + +
    + + + + + + + + + + + + + + + + + + + + +
    + + Full Name + + + ${name} +
    + + Institute + + + ${institute} +
    + + Email Address + + + ${emailAddress} +
    + + Comments + + + ${comments} +
    +
    + +Accept/reject this request on the admin panel: ${adminAccountRequestsPageUrl} + +

    + Regards,
    + TEAMMATES Team. +

    diff --git a/src/main/resources/db/changelog/db.changelog-root.xml b/src/main/resources/db/changelog/db.changelog-root.xml index 66d4b7d7c88e..bdacf10c68dd 100644 --- a/src/main/resources/db/changelog/db.changelog-root.xml +++ b/src/main/resources/db/changelog/db.changelog-root.xml @@ -4,5 +4,6 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> - + + diff --git a/src/main/resources/db/changelog/db.changelog-v9.0.0-beta.5.xml b/src/main/resources/db/changelog/db.changelog-v9.0.0-beta.5.xml new file mode 100644 index 000000000000..14642683cb17 --- /dev/null +++ b/src/main/resources/db/changelog/db.changelog-v9.0.0-beta.5.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog/db.changelog-v9.0.0.xml b/src/main/resources/db/changelog/db.changelog-v9.0.0.xml new file mode 100644 index 000000000000..94c02f08ed65 --- /dev/null +++ b/src/main/resources/db/changelog/db.changelog-v9.0.0.xml @@ -0,0 +1,539 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog/db.changelog-v9.xml b/src/main/resources/db/changelog/db.changelog-v9.xml deleted file mode 100644 index 57b6e7f9587c..000000000000 --- a/src/main/resources/db/changelog/db.changelog-v9.xml +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/instructorEmailTemplate-newAccountRequestAcknowledgement.html b/src/main/resources/instructorEmailTemplate-newAccountRequestAcknowledgement.html new file mode 100644 index 000000000000..ad1fe08a704d --- /dev/null +++ b/src/main/resources/instructorEmailTemplate-newAccountRequestAcknowledgement.html @@ -0,0 +1,65 @@ +

    Hello, ${name}

    + +

    + Thank you for submitting an account request. This is what you have submitted: +

    + +
    + + + + + + + + + + + + + + + + + + + + +
    + + Full Name + + + ${name} +
    + + Country & Institute + + + ${institute} +
    + + Email Address + + + ${emailAddress} +
    + + Comments + + + ${comments} +
    +
    + +

    + Your request will be reviewed within 24 hours. We will send another email once your request has been accepted. +

    +

    + If you have any additional queries, please feel free to contact us at ${supportEmail}. +

    + +

    + Regards,
    + TEAMMATES Team. +

    diff --git a/src/main/resources/logsForLocalDev.json b/src/main/resources/logsForLocalDev.json index 66d5c04a1186..ed155eb3581e 100644 --- a/src/main/resources/logsForLocalDev.json +++ b/src/main/resources/logsForLocalDev.json @@ -28,8 +28,8 @@ "teammates.ui.webapi.CreateCourseAction.execute(CreateCourseAction.java:18)", "teammates.ui.webapi.WebApiServlet.invokeServlet(WebApiServlet.java:70)", "teammates.ui.webapi.WebApiServlet.doPost(WebApiServlet.java:48)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:707)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:790)", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:707)", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790)", "..." ], [ @@ -114,8 +114,8 @@ "teammates.ui.webapi.CreateCourseAction.execute(CreateCourseAction.java:18)", "teammates.ui.webapi.WebApiServlet.invokeServlet(WebApiServlet.java:70)", "teammates.ui.webapi.WebApiServlet.doPost(WebApiServlet.java:48)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:707)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:790)", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:707)", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790)", "..." ], [ @@ -197,8 +197,8 @@ "teammates.ui.webapi.SearchInstructorsAction.execute(SearchInstructorsAction.java:19)", "teammates.ui.webapi.WebApiServlet.invokeServlet(WebApiServlet.java:70)", "teammates.ui.webapi.WebApiServlet.doGet(WebApiServlet.java:43)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:687)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:790),", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:687)", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790),", "..." ], [ @@ -270,8 +270,8 @@ "teammates.ui.webapi.QueryLogsAction.execute(QueryLogsAction.java:42)", "teammates.ui.webapi.WebApiServlet.invokeServlet(WebApiServlet.java:70)", "teammates.ui.webapi.WebApiServlet.doGet(WebApiServlet.java:43)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:687)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:790),", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:687)", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790),", "..." ] ], @@ -316,8 +316,8 @@ "teammates.ui.webapi.QueryLogsAction.execute(QueryLogsAction.java:42)", "teammates.ui.webapi.WebApiServlet.invokeServlet(WebApiServlet.java:70)", "teammates.ui.webapi.WebApiServlet.doGet(WebApiServlet.java:43)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:687)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:790),", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:687)", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790),", "..." ] ], @@ -363,8 +363,8 @@ "teammates.ui.webapi.CompileLogsAction.execute(CompileLogsAction.java:11)", "teammates.ui.webapi.WebApiServlet.invokeServlet(WebApiServlet.java:70)", "teammates.ui.webapi.WebApiServlet.doGet(WebApiServlet.java:43)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:687)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:790),", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:687)", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790),", "..." ] ], @@ -410,8 +410,8 @@ "teammates.ui.webapi.CompileLogsAction.execute(CompileLogsAction.java:11)", "teammates.ui.webapi.WebApiServlet.invokeServlet(WebApiServlet.java:70)", "teammates.ui.webapi.WebApiServlet.doGet(WebApiServlet.java:43)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:687)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:790),", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:687)", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790),", "..." ] ], @@ -457,8 +457,8 @@ "teammates.ui.webapi.CompileLogsAction.execute(CompileLogsAction.java:11)", "teammates.ui.webapi.WebApiServlet.invokeServlet(WebApiServlet.java:70)", "teammates.ui.webapi.WebApiServlet.doGet(WebApiServlet.java:43)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:687)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:790),", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:687)", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790),", "..." ] ], @@ -501,9 +501,9 @@ "teammates.ui.webapi.ActionFactory.getAction(ActionFactory.java:163)", "teammates.ui.webapi.WebApiServlet.invokeServlet(WebApiServlet.java:67)", "teammates.ui.webapi.WebApiServlet.doGet(WebApiServlet.java:44)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:687)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:790),", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:790),", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:687)", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790),", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790),", "..." ] ], @@ -546,9 +546,9 @@ "teammates.ui.webapi.ActionFactory.getAction(ActionFactory.java:163)", "teammates.ui.webapi.WebApiServlet.invokeServlet(WebApiServlet.java:67)", "teammates.ui.webapi.WebApiServlet.doGet(WebApiServlet.java:44)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:687)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:790),", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:790),", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:687)", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790),", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790),", "..." ] ], diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index f2033dc9143b..7194f33e6f07 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -40,7 +40,7 @@ ObjectifyFilter - com.googlecode.objectify.ObjectifyFilter + teammates.ui.servlets.ObjectifyFilter ObjectifyFilter diff --git a/src/test/java/teammates/architecture/ArchitectureTest.java b/src/test/java/teammates/architecture/ArchitectureTest.java index 1113db613728..040a1f907793 100644 --- a/src/test/java/teammates/architecture/ArchitectureTest.java +++ b/src/test/java/teammates/architecture/ArchitectureTest.java @@ -49,6 +49,7 @@ public class ArchitectureTest { private static final String LNP_PACKAGE = "teammates.lnp"; private static final String LNP_CASES_PACKAGE = LNP_PACKAGE + ".cases"; + private static final String LNP_SQL_PACKAGE = LNP_PACKAGE + ".sql"; private static final String LNP_UTIL_PACKAGE = LNP_PACKAGE + ".util"; private static final String CLIENT_PACKAGE = "teammates.client"; @@ -413,7 +414,13 @@ public void testArchitecture_lnp_lnpShouldBeSelfContained() { @Test public void testArchitecture_lnp_lnpShouldNotTouchProductionCodeExceptCommonAndRequests() { noClasses().that().resideInAPackage(includeSubpackages(LNP_PACKAGE)) - .should().accessClassesThat().resideInAPackage(includeSubpackages(STORAGE_PACKAGE)) + .should().accessClassesThat(new DescribedPredicate<>("") { + @Override + public boolean apply(JavaClass input) { + return input.getPackageName().startsWith(STORAGE_PACKAGE) + && !input.getPackageName().startsWith(STORAGE_SQL_ENTITY_PACKAGE); + } + }) .orShould().accessClassesThat().resideInAPackage(includeSubpackages(LOGIC_PACKAGE)) .orShould().accessClassesThat(new DescribedPredicate<>("") { @Override @@ -442,6 +449,23 @@ public boolean apply(JavaClass input) { }).check(forClasses(LNP_CASES_PACKAGE)); } + @Test + public void testArchitecture_lnp_lnpSqlTestCasesShouldBeIndependentOfEachOther() { + noClasses().that(new DescribedPredicate<>("") { + @Override + public boolean apply(JavaClass input) { + return input.getPackageName().startsWith(LNP_SQL_PACKAGE) && !input.isInnerClass(); + } + }).should().accessClassesThat(new DescribedPredicate<>("") { + @Override + public boolean apply(JavaClass input) { + return input.getPackageName().startsWith(LNP_SQL_PACKAGE) + && !input.getSimpleName().startsWith("Base") + && !input.isInnerClass(); + } + }).check(forClasses(LNP_SQL_PACKAGE)); + } + @Test public void testArchitecture_lnp_lnpShouldNotHaveAnyDependency() { noClasses().that().resideInAPackage(includeSubpackages(LNP_UTIL_PACKAGE)) @@ -527,6 +551,7 @@ public void testArchitecture_externalApi_objectifyApiCanOnlyBeAccessedBySomePack .and().resideOutsideOfPackage(includeSubpackages(CLIENT_SCRIPTS_PACKAGE)) .and().doNotHaveSimpleName("BaseTestCaseWithSqlDatabaseAccess") .and().doNotHaveSimpleName("BaseTestCaseWithLocalDatabaseAccess") + .and().doNotHaveSimpleName("ObjectifyFilter") .should().accessClassesThat().resideInAPackage("com.googlecode.objectify..") .check(ALL_CLASSES); } @@ -540,7 +565,7 @@ public void testArchitecture_externalApi_servletApiCanOnlyBeAccessedBySomePackag .and().doNotHaveSimpleName("MockHttpServletResponse") .and().doNotHaveSimpleName("MockPart") .and().resideOutsideOfPackage(includeSubpackages(UI_WEBAPI_PACKAGE)) - .should().accessClassesThat().haveFullyQualifiedName("javax.servlet..") + .should().accessClassesThat().haveFullyQualifiedName("jakarta.servlet..") .check(ALL_CLASSES); } @@ -557,7 +582,7 @@ public void testArchitecture_externalApi_assertionApiCanOnlyBeAccessedBySomePack .and().doNotHaveSimpleName("BaseTestCase") .and().doNotHaveSimpleName("AssertHelper") .and().doNotHaveSimpleName("EmailChecker") - .should().accessClassesThat().haveFullyQualifiedName("org.junit.Assert") + .should().accessClassesThat().haveFullyQualifiedName("org.junit.jupiter.api.Assertions") .check(ALL_CLASSES); } diff --git a/src/test/java/teammates/common/datatransfer/ErrorLogEntryTest.java b/src/test/java/teammates/common/datatransfer/ErrorLogEntryTest.java index 4092891e80c8..1554babf2c7b 100644 --- a/src/test/java/teammates/common/datatransfer/ErrorLogEntryTest.java +++ b/src/test/java/teammates/common/datatransfer/ErrorLogEntryTest.java @@ -64,9 +64,9 @@ public void testFromLogEntry_exceptionLogDetails_shouldGetPrettyPrintedLog() { "teammates.ui.webapi.ActionFactory.getAction(ActionFactory.java:163)", "teammates.ui.webapi.WebApiServlet.invokeServlet(WebApiServlet.java:67)", "teammates.ui.webapi.WebApiServlet.doGet(WebApiServlet.java:44)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:687)", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:790),", - "javax.servlet.http.HttpServlet.service(HttpServlet.java:790),", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:687)", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790),", + "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790),", "..." ) )); @@ -81,9 +81,9 @@ public void testFromLogEntry_exceptionLogDetails_shouldGetPrettyPrintedLog() { " at teammates.ui.webapi.ActionFactory.getAction(ActionFactory.java:163)", " at teammates.ui.webapi.WebApiServlet.invokeServlet(WebApiServlet.java:67)", " at teammates.ui.webapi.WebApiServlet.doGet(WebApiServlet.java:44)", - " at javax.servlet.http.HttpServlet.service(HttpServlet.java:687)", - " at javax.servlet.http.HttpServlet.service(HttpServlet.java:790),", - " at javax.servlet.http.HttpServlet.service(HttpServlet.java:790),", + " at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:687)", + " at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790),", + " at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:790),", " at ...", "" )); diff --git a/src/test/java/teammates/common/util/FieldValidatorTest.java b/src/test/java/teammates/common/util/FieldValidatorTest.java index e5b3d9748b58..88fc7f33595b 100644 --- a/src/test/java/teammates/common/util/FieldValidatorTest.java +++ b/src/test/java/teammates/common/util/FieldValidatorTest.java @@ -210,11 +210,12 @@ public void testGetInvalidityInfoForInstituteName_invalid_returnSpecificErrorStr String invalidInstituteName = StringHelperExtension.generateStringOfLength( FieldValidator.INSTITUTE_NAME_MAX_LENGTH + 1); String actual = FieldValidator.getInvalidityInfoForInstituteName(invalidInstituteName); + String expectedTemplate = "\"%s\" is not " + + "acceptable to TEAMMATES as a/an institute name because it is too long. The value " + + "of a/an institute name should be no longer than 128 characters. It should not be empty."; + String expected = String.format(expectedTemplate, invalidInstituteName); assertEquals("Invalid institute name (too long) should return error message that is specific to institute name", - "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\" is not " - + "acceptable to TEAMMATES as a/an institute name because it is too long. The value " - + "of a/an institute name should be no longer than 64 characters. It should not be empty.", - actual); + expected, actual); } @Test diff --git a/src/test/java/teammates/common/util/JsonUtilsTest.java b/src/test/java/teammates/common/util/JsonUtilsTest.java index 57477d7a599a..a1e3c6156987 100644 --- a/src/test/java/teammates/common/util/JsonUtilsTest.java +++ b/src/test/java/teammates/common/util/JsonUtilsTest.java @@ -1,6 +1,7 @@ package teammates.common.util; import java.util.ArrayList; +import java.util.List; import org.testng.annotations.Test; @@ -20,7 +21,7 @@ public class JsonUtilsTest extends BaseTestCase { public void testFeedbackQuestionDetailsAdaptor_withComposedQuestionDetails_shouldSerializeToConcreteClass() { FeedbackTextQuestionDetails questionDetails = new FeedbackTextQuestionDetails("Question text."); - ArrayList participants = new ArrayList<>(); + List participants = new ArrayList<>(); participants.add(FeedbackParticipantType.OWN_TEAM_MEMBERS); participants.add(FeedbackParticipantType.RECEIVER); diff --git a/src/test/java/teammates/common/util/TimeHelperTest.java b/src/test/java/teammates/common/util/TimeHelperTest.java index 3c805252f979..9f9ca7035abb 100644 --- a/src/test/java/teammates/common/util/TimeHelperTest.java +++ b/src/test/java/teammates/common/util/TimeHelperTest.java @@ -146,4 +146,68 @@ public void testGetInstantMonthsOffsetFromNow() { assertEquals(expected, actual); } + @Test + public void getInstantNearestQuarterHourBefore() { + Instant expectedQ1 = Instant.parse("2020-12-31T16:00:00Z"); + Instant actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:00:00Z")); + + assertEquals(expectedQ1, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:09:30Z")); + + assertEquals(expectedQ1, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:14:59Z")); + + assertEquals(expectedQ1, actual); + + actual = TimeHelper + .getInstantNearestQuarterHourBefore(OffsetDateTime.parse("2021-01-01T00:10:00+08:00").toInstant()); + + assertEquals(expectedQ1, actual); + + actual = TimeHelper + .getInstantNearestQuarterHourBefore(OffsetDateTime.parse("2020-12-31T12:09:00-04:00").toInstant()); + + assertEquals(expectedQ1, actual); + + Instant expectedQ2 = Instant.parse("2020-12-31T16:15:00Z"); + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:15:00Z")); + + assertEquals(expectedQ2, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:19:30Z")); + + assertEquals(expectedQ2, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:29:59Z")); + + assertEquals(expectedQ2, actual); + + Instant expectedQ3 = Instant.parse("2020-12-31T16:30:00Z"); + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:30:00Z")); + + assertEquals(expectedQ3, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:39:30Z")); + + assertEquals(expectedQ3, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:44:59Z")); + + assertEquals(expectedQ3, actual); + + Instant expectedQ4 = Instant.parse("2020-12-31T16:45:00Z"); + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:45:00Z")); + + assertEquals(expectedQ4, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:49:30Z")); + + assertEquals(expectedQ4, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:59:59Z")); + + assertEquals(expectedQ4, actual); + } } diff --git a/src/test/java/teammates/logic/api/MockLogsProcessor.java b/src/test/java/teammates/logic/api/MockLogsProcessor.java index dc8a90ed9fda..60299b538be6 100644 --- a/src/test/java/teammates/logic/api/MockLogsProcessor.java +++ b/src/test/java/teammates/logic/api/MockLogsProcessor.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.UUID; import teammates.common.datatransfer.FeedbackSessionLogEntry; import teammates.common.datatransfer.QueryLogsResults; @@ -23,9 +24,19 @@ public class MockLogsProcessor extends LogsProcessor { /** * Simulates insertion of feedback session logs. */ - public void insertFeedbackSessionLog(String studentEmail, String feedbackSessionName, + public void insertFeedbackSessionLog(String courseId, String studentEmail, String feedbackSessionName, String fslType, long timestamp) { - feedbackSessionLogs.add(new FeedbackSessionLogEntry(studentEmail, feedbackSessionName, fslType, timestamp)); + feedbackSessionLogs + .add(new FeedbackSessionLogEntry(courseId, studentEmail, feedbackSessionName, fslType, timestamp)); + } + + /** + * Simulates insertion of feedback session logs. + */ + public void insertFeedbackSessionLog(String courseId, UUID studentId, UUID feedbackSessionId, + String fslType, long timestamp) { + feedbackSessionLogs + .add(new FeedbackSessionLogEntry(courseId, studentId, feedbackSessionId, fslType, timestamp)); } /** @@ -97,13 +108,14 @@ public QueryLogsResults queryLogs(QueryLogsParams queryLogsParams) { } @Override - public void createFeedbackSessionLog(String courseId, String email, String fsName, String fslType) { + public void createFeedbackSessionLog(String courseId, UUID studentId, UUID fsId, String fslType) { // No-op } @Override - public List getFeedbackSessionLogs(String courseId, String email, + public List getOrderedFeedbackSessionLogs(String courseId, String email, long startTime, long endTime, String fsName) { + feedbackSessionLogs.sort((x, y) -> x.compareTo(y)); return feedbackSessionLogs; } diff --git a/src/test/java/teammates/logic/api/MockUserProvision.java b/src/test/java/teammates/logic/api/MockUserProvision.java index 7fa2fdb97f7d..f6a88082bcd0 100644 --- a/src/test/java/teammates/logic/api/MockUserProvision.java +++ b/src/test/java/teammates/logic/api/MockUserProvision.java @@ -30,6 +30,22 @@ public UserInfo loginUser(String userId) { return loginUser(userId, false); } + private UserInfo loginUserWithTransaction(String userId, boolean isAdmin) { + isLoggedIn = true; + mockUser.id = userId; + mockUser.isAdmin = isAdmin; + return getCurrentUserWithTransaction(null); + } + + /** + * Adds a logged-in user without admin rights. + * + * @return The user info after login process + */ + public UserInfo loginUserWithTransaction(String userId) { + return loginUserWithTransaction(userId, false); + } + /** * Adds a logged-in user as an admin. * @@ -39,6 +55,15 @@ public UserInfo loginAsAdmin(String userId) { return loginUser(userId, true); } + /** + * Adds a logged-in user as an admin. + * + * @return The user info after login process + */ + public UserInfo loginAsAdminWithTransaction(String userId) { + return loginUserWithTransaction(userId, true); + } + /** * Removes the logged-in user information. */ diff --git a/src/test/java/teammates/logic/core/AccountRequestsLogicTest.java b/src/test/java/teammates/logic/core/AccountRequestsLogicTest.java index 88cfd8430b38..24a66afbe6dd 100644 --- a/src/test/java/teammates/logic/core/AccountRequestsLogicTest.java +++ b/src/test/java/teammates/logic/core/AccountRequestsLogicTest.java @@ -9,6 +9,7 @@ import teammates.common.exception.InvalidParametersException; import teammates.common.util.Const; import teammates.common.util.FieldValidator; +import teammates.storage.entity.AccountRequest; import teammates.test.AssertHelper; /** @@ -108,7 +109,10 @@ public void testUpdateAccountRequest() throws Exception { @Test public void testDeleteAccountRequest() throws Exception { - AccountRequestAttributes a = dataBundle.accountRequests.get("unregisteredInstructor1"); + // This ensures the AccountRequestAttributes has the correct ID. + AccountRequestAttributes accountRequestAttributes = dataBundle.accountRequests.get("unregisteredInstructor1"); + AccountRequest accountRequest = accountRequestAttributes.toEntity(); + AccountRequestAttributes a = AccountRequestAttributes.valueOf(accountRequest); ______TS("silent deletion of non-existent account request"); diff --git a/src/test/java/teammates/logic/core/NotificationsLogicTest.java b/src/test/java/teammates/logic/core/NotificationsLogicTest.java index 44e5c79b8442..1aaf53efd015 100644 --- a/src/test/java/teammates/logic/core/NotificationsLogicTest.java +++ b/src/test/java/teammates/logic/core/NotificationsLogicTest.java @@ -23,6 +23,7 @@ public class NotificationsLogicTest extends BaseLogicTest { private NotificationAttributes n; private final NotificationsDb notifDb = NotificationsDb.inst(); private final NotificationsLogic notifLogic = NotificationsLogic.inst(); + @SuppressWarnings("PMD.FinalFieldCouldBeStatic") private final Map typicalNotifications = getTypicalDataBundle().notifications; @Override diff --git a/src/test/java/teammates/logic/external/GoogleRecaptchaServiceTest.java b/src/test/java/teammates/logic/external/GoogleRecaptchaServiceTest.java index 6a418a84e9a1..8066cf6b4a11 100644 --- a/src/test/java/teammates/logic/external/GoogleRecaptchaServiceTest.java +++ b/src/test/java/teammates/logic/external/GoogleRecaptchaServiceTest.java @@ -57,7 +57,7 @@ public void testIsVerificationSuccessful() { * * @see reCAPTCHA API error codes */ - private static class GoogleRecaptchaServiceStub extends GoogleRecaptchaService { + private static final class GoogleRecaptchaServiceStub extends GoogleRecaptchaService { private GoogleRecaptchaServiceStub(String secretKey) { super(secretKey); diff --git a/src/test/java/teammates/sqllogic/api/SqlEmailGeneratorTest.java b/src/test/java/teammates/sqllogic/api/SqlEmailGeneratorTest.java new file mode 100644 index 000000000000..f984b10f46c5 --- /dev/null +++ b/src/test/java/teammates/sqllogic/api/SqlEmailGeneratorTest.java @@ -0,0 +1,122 @@ +package teammates.sqllogic.api; + +import java.io.IOException; + +import org.testng.annotations.Test; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.util.Config; +import teammates.common.util.EmailType; +import teammates.common.util.EmailWrapper; +import teammates.storage.sqlentity.AccountRequest; +import teammates.test.BaseTestCase; +import teammates.test.EmailChecker; + +/** + * SUT: {@link SqlEmailGenerator}. + */ +public class SqlEmailGeneratorTest extends BaseTestCase { + private final SqlEmailGenerator sqlEmailGenerator = SqlEmailGenerator.inst(); + + @Test + void testGenerateNewAccountRequestAdminAlertEmail_withComments_generatesSuccessfully() throws IOException { + AccountRequest accountRequest = new AccountRequest("chosen-one@jedi.org", "Anakin Skywalker", "Jedi Order", + AccountRequestStatus.PENDING, + "I don't like sand. It's coarse and rough and irritating... and it gets everywhere."); + EmailWrapper email = sqlEmailGenerator.generateNewAccountRequestAdminAlertEmail(accountRequest); + verifyEmail(email, Config.SUPPORT_EMAIL, EmailType.NEW_ACCOUNT_REQUEST_ADMIN_ALERT, + "TEAMMATES (Action Needed): New Account Request Received", + "/adminNewAccountRequestAlertEmailWithComments.html"); + } + + @Test + void testGenerateNewAccountRequestAdminAlertEmail_withNoComments_generatesSuccessfully() throws IOException { + AccountRequest accountRequest = new AccountRequest("maul@sith.org", "Maul", "Sith Order", + AccountRequestStatus.PENDING, null); + EmailWrapper email = sqlEmailGenerator.generateNewAccountRequestAdminAlertEmail(accountRequest); + verifyEmail(email, Config.SUPPORT_EMAIL, EmailType.NEW_ACCOUNT_REQUEST_ADMIN_ALERT, + "TEAMMATES (Action Needed): New Account Request Received", + "/adminNewAccountRequestAlertEmailWithNoComments.html"); + } + + @Test + void testGenerateNewAccountRequestAcknowledgementEmail_withComments_generatesSuccessfully() throws IOException { + AccountRequest accountRequest = new AccountRequest("darth-vader@sith.org", "Darth Vader", "Sith Order", + AccountRequestStatus.PENDING, + "I Am Your Father"); + EmailWrapper email = sqlEmailGenerator.generateNewAccountRequestAcknowledgementEmail(accountRequest); + verifyEmail(email, "darth-vader@sith.org", EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT, + "TEAMMATES: Acknowledgement of Instructor Account Request", + Config.SUPPORT_EMAIL, + "/instructorNewAccountRequestAcknowledgementEmailWithComments.html"); + } + + @Test + void testGenerateNewAccountRequestAcknowledgementEmail_withNoComments_generatesSuccessfully() throws IOException { + AccountRequest accountRequest = new AccountRequest("maul@sith.org", "Maul", "Sith Order", + AccountRequestStatus.PENDING, null); + EmailWrapper email = sqlEmailGenerator.generateNewAccountRequestAcknowledgementEmail(accountRequest); + verifyEmail(email, "maul@sith.org", EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT, + "TEAMMATES: Acknowledgement of Instructor Account Request", + Config.SUPPORT_EMAIL, + "/instructorNewAccountRequestAcknowledgementEmailWithNoComments.html"); + } + + @Test + void testGenerateAccountRequestRejectionEmail_withDefaultReason_generatesSuccessfully() throws IOException { + AccountRequest accountRequest = new AccountRequest("maul@sith.org", "Maul", "Sith Order", + AccountRequestStatus.PENDING, null); + String title = "We are Unable to Create an Account for you"; + String content = new StringBuilder() + .append("

    Hi, Maul

    \n") + .append("

    Thanks for your interest in using TEAMMATES. ") + .append("We are unable to create a TEAMMATES instructor account for you.

    \n\n") + .append("

    \n") + .append(" Reason: The email address you provided ") + .append("is not an 'official' email address provided by your institution.
    \n") + .append(" Remedy: ") + .append("Please re-submit an account request with your 'official' institution email address.\n") + .append("

    \n\n") + .append("

    If you need further clarification or would like to appeal this decision, ") + .append("please feel free to contact us at teammates@comp.nus.edu.sg.

    \n") + .append("

    Regards,
    TEAMMATES Team.

    \n") + .toString(); + + EmailWrapper email = sqlEmailGenerator.generateAccountRequestRejectionEmail(accountRequest, title, content); + verifyEmail(email, "maul@sith.org", EmailType.ACCOUNT_REQUEST_REJECTION, + "TEAMMATES: " + title, + Config.SUPPORT_EMAIL, + "/instructorAccountRequestRejectionEmail.html"); + } + + private void verifyEmail(EmailWrapper email, String expectedRecipientEmailAddress, EmailType expectedEmailType, + String expectedSubject, String expectedEmailContentFilePathname) throws IOException { + assertEquals(expectedRecipientEmailAddress, email.getRecipient()); + assertEquals(Config.EMAIL_SENDEREMAIL, email.getSenderEmail()); + assertEquals(Config.EMAIL_SENDERNAME, email.getSenderName()); + assertEquals(Config.EMAIL_REPLYTO, email.getReplyTo()); + assertEquals(expectedEmailType, email.getType()); + assertEquals(expectedSubject, email.getSubject()); + String emailContent = email.getContent(); + EmailChecker.verifyEmailContent(emailContent, expectedEmailContentFilePathname); + verifyEmailContentHasNoPlaceholders(emailContent); + } + + private void verifyEmail(EmailWrapper email, String expectedRecipientEmailAddress, EmailType expectedEmailType, + String expectedSubject, String expectedBcc, String expectedEmailContentFilePathname) throws IOException { + assertEquals(expectedRecipientEmailAddress, email.getRecipient()); + assertEquals(Config.EMAIL_SENDEREMAIL, email.getSenderEmail()); + assertEquals(Config.EMAIL_SENDERNAME, email.getSenderName()); + assertEquals(Config.EMAIL_REPLYTO, email.getReplyTo()); + assertEquals(expectedEmailType, email.getType()); + assertEquals(expectedSubject, email.getSubject()); + assertEquals(expectedBcc, email.getBcc()); + String emailContent = email.getContent(); + EmailChecker.verifyEmailContent(emailContent, expectedEmailContentFilePathname); + verifyEmailContentHasNoPlaceholders(emailContent); + } + + private void verifyEmailContentHasNoPlaceholders(String emailContent) { + assertFalse(emailContent.contains("${")); + } +} diff --git a/src/test/java/teammates/sqllogic/core/AccountRequestsLogicTest.java b/src/test/java/teammates/sqllogic/core/AccountRequestsLogicTest.java new file mode 100644 index 000000000000..a0a3064ae32a --- /dev/null +++ b/src/test/java/teammates/sqllogic/core/AccountRequestsLogicTest.java @@ -0,0 +1,182 @@ +package teammates.sqllogic.core; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.UUID; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.exception.EntityDoesNotExistException; +import teammates.common.exception.InvalidParametersException; +import teammates.common.util.Const; +import teammates.storage.sqlapi.AccountRequestsDb; +import teammates.storage.sqlentity.AccountRequest; +import teammates.test.BaseTestCase; + +/** + * SUT: {@link AccountRequestsLogic}. + */ +public class AccountRequestsLogicTest extends BaseTestCase { + + private AccountRequestsLogic accountRequestsLogic = AccountRequestsLogic.inst(); + private AccountRequestsDb accountRequestsDb; + + @BeforeMethod + public void setUpMethod() { + accountRequestsDb = mock(AccountRequestsDb.class); + accountRequestsLogic.initLogicDependencies(accountRequestsDb); + } + + @Test + public void testCreateAccountRequest_typicalRequest_success() throws Exception { + AccountRequest accountRequest = getTypicalAccountRequest(); + when(accountRequestsDb.createAccountRequest(accountRequest)).thenReturn(accountRequest); + AccountRequest createdAccountRequest = accountRequestsLogic.createAccountRequest(accountRequest); + + assertEquals(accountRequest, createdAccountRequest); + verify(accountRequestsDb, times(1)).createAccountRequest(accountRequest); + } + + @Test + public void testCreateAccountRequest_requestAlreadyExists_success() throws Exception { + AccountRequest accountRequest1 = getTypicalAccountRequest(); + AccountRequest accountRequest2 = getTypicalAccountRequest(); + when(accountRequestsDb.createAccountRequest(accountRequest1)) + .thenReturn(accountRequest1); + when(accountRequestsDb.createAccountRequest(accountRequest2)) + .thenReturn(accountRequest2); + + accountRequestsLogic.createAccountRequest(accountRequest1); + accountRequestsLogic.createAccountRequest(accountRequest2); + verify(accountRequestsDb, times(1)).createAccountRequest(accountRequest1); + verify(accountRequestsDb, times(1)).createAccountRequest(accountRequest2); + } + + @Test + public void testCreateAccountRequest_invalidParams_failure() throws Exception { + AccountRequest invalidEmailAccountRequest = getTypicalAccountRequest(); + invalidEmailAccountRequest.setEmail("invalid email"); + when(accountRequestsDb.createAccountRequest(invalidEmailAccountRequest)) + .thenThrow(new InvalidParametersException("test exception")); + + assertThrows(InvalidParametersException.class, () -> { + accountRequestsLogic.createAccountRequest(invalidEmailAccountRequest); + }); + verify(accountRequestsDb, times(1)).createAccountRequest(invalidEmailAccountRequest); + } + + @Test + public void testUpdateAccountRequest_typicalRequest_success() + throws InvalidParametersException, EntityDoesNotExistException { + AccountRequest ar = getTypicalAccountRequest(); + when(accountRequestsDb.updateAccountRequest(ar)).thenReturn(ar); + AccountRequest updatedAr = accountRequestsLogic.updateAccountRequest(ar); + + assertEquals(ar, updatedAr); + verify(accountRequestsDb, times(1)).updateAccountRequest(ar); + } + + @Test + public void testUpdateAccountRequest_requestNotFound_failure() + throws InvalidParametersException, EntityDoesNotExistException { + AccountRequest arNotFound = getTypicalAccountRequest(); + when(accountRequestsDb.updateAccountRequest(arNotFound)).thenThrow(new EntityDoesNotExistException("test message")); + + assertThrows(EntityDoesNotExistException.class, + () -> accountRequestsLogic.updateAccountRequest(arNotFound)); + verify(accountRequestsDb, times(1)).updateAccountRequest(any(AccountRequest.class)); + } + + @Test + public void testDeleteAccountRequest_typicalRequest_success() { + AccountRequest ar = getTypicalAccountRequest(); + when(accountRequestsDb.getAccountRequest(ar.getId())).thenReturn(ar); + accountRequestsLogic.deleteAccountRequest(ar.getId()); + + verify(accountRequestsDb, times(1)).deleteAccountRequest(any(AccountRequest.class)); + } + + @Test + public void testDeleteAccountRequest_nonexistentRequest_shouldSilentlyDelete() { + UUID nonexistentUuid = UUID.fromString("00000000-0000-4000-8000-000000000100"); + accountRequestsLogic.deleteAccountRequest(nonexistentUuid); + + verify(accountRequestsDb, times(1)).deleteAccountRequest(nullable(AccountRequest.class)); + } + + @Test + public void testGetAccountRequestByRegistrationKey_typicalRequest_success() { + AccountRequest ar = getTypicalAccountRequest(); + String regkey = "regkey"; + ar.setRegistrationKey(regkey); + when(accountRequestsDb.getAccountRequestByRegistrationKey(regkey)).thenReturn(ar); + AccountRequest actualAr = + accountRequestsLogic.getAccountRequestByRegistrationKey(ar.getRegistrationKey()); + + assertEquals(ar, actualAr); + verify(accountRequestsDb, times(1)).getAccountRequestByRegistrationKey(regkey); + } + + @Test + public void testGetAccountRequestByRegistrationKey_nonexistentRequest_shouldReturnNull() throws Exception { + String nonexistentRegkey = "not_exist"; + when(accountRequestsDb.getAccountRequestByRegistrationKey(nonexistentRegkey)).thenReturn(null); + + assertNull(accountRequestsLogic.getAccountRequestByRegistrationKey(nonexistentRegkey)); + verify(accountRequestsDb, times(1)).getAccountRequestByRegistrationKey(nonexistentRegkey); + } + + @Test + public void testResetAccountRequest_typicalRequest_success() + throws InvalidParametersException, EntityDoesNotExistException { + AccountRequest accountRequest = getTypicalAccountRequest(); + accountRequest.setRegisteredAt(Const.TIME_REPRESENTS_NOW); + when(accountRequestsDb.getAccountRequest(accountRequest.getId())) + .thenReturn(accountRequest); + when(accountRequestsDb.updateAccountRequest(accountRequest)).thenReturn(accountRequest); + accountRequest = accountRequestsLogic.resetAccountRequest(accountRequest.getId()); + + assertNull(accountRequest.getRegisteredAt()); + verify(accountRequestsDb, times(1)).getAccountRequest(accountRequest.getId()); + } + + @Test + public void testResetAccountRequest_nonexistentRequest_failure() + throws InvalidParametersException, EntityDoesNotExistException { + AccountRequest accountRequest = getTypicalAccountRequest(); + accountRequest.setRegisteredAt(Const.TIME_REPRESENTS_NOW); + when(accountRequestsDb.getAccountRequest(accountRequest.getId())) + .thenReturn(null); + assertThrows(EntityDoesNotExistException.class, + () -> accountRequestsLogic.resetAccountRequest(accountRequest.getId())); + verify(accountRequestsDb, times(1)).getAccountRequest(accountRequest.getId()); + verify(accountRequestsDb, times(0)).updateAccountRequest(nullable(AccountRequest.class)); + } + + @Test + public void testGetAccountRequest_nonExistentAccountRequest_returnsNull() { + UUID id = UUID.randomUUID(); + when(accountRequestsDb.getAccountRequest(id)).thenReturn(null); + AccountRequest actualAccountRequest = accountRequestsLogic.getAccountRequest(id); + verify(accountRequestsDb).getAccountRequest(id); + assertNull(actualAccountRequest); + } + + @Test + public void testGetAccountRequest_existingAccountRequest_getsSuccessfully() { + AccountRequest expectedAccountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); + UUID id = expectedAccountRequest.getId(); + when(accountRequestsDb.getAccountRequest(id)).thenReturn(expectedAccountRequest); + AccountRequest actualAccountRequest = accountRequestsLogic.getAccountRequest(id); + verify(accountRequestsDb).getAccountRequest(id); + assertEquals(expectedAccountRequest, actualAccountRequest); + } +} diff --git a/src/test/java/teammates/sqllogic/core/FeedbackQuestionsLogicTest.java b/src/test/java/teammates/sqllogic/core/FeedbackQuestionsLogicTest.java index fb7b75ceed12..4b77e1d75d76 100644 --- a/src/test/java/teammates/sqllogic/core/FeedbackQuestionsLogicTest.java +++ b/src/test/java/teammates/sqllogic/core/FeedbackQuestionsLogicTest.java @@ -73,7 +73,7 @@ public void testGetFeedbackQuestionsForSession_questionNumbersOutOfOrder_success fq4.setQuestionNumber(4); fq5.setQuestionNumber(5); - ArrayList questions = new ArrayList<>(List.of(fq2, fq4, fq3, fq1, fq5)); + List questions = new ArrayList<>(List.of(fq2, fq4, fq3, fq1, fq5)); fs.setId(UUID.randomUUID()); when(fqDb.getFeedbackQuestionsForSession(fs.getId())).thenReturn(questions); @@ -116,7 +116,7 @@ public void testCreateFeedbackQuestion_questionNumbersAreInconsistent_canCreateF fq3.setQuestionNumber(4); fq4.setQuestionNumber(5); - ArrayList questionsBefore = new ArrayList<>(List.of(fq1, fq2, fq3, fq4)); + List questionsBefore = new ArrayList<>(List.of(fq1, fq2, fq3, fq4)); fs.setId(UUID.randomUUID()); when(fqDb.getFeedbackQuestionsForSession(fs.getId())).thenReturn(questionsBefore); when(fqDb.createFeedbackQuestion(fq5)).thenReturn(fq5); @@ -142,7 +142,7 @@ public void testCreateFeedbackQuestion_oldQuestionNumberLargerThanNewQuestionNum fq4.setQuestionNumber(5); fq5.setQuestionNumber(1); - ArrayList questionsBefore = new ArrayList<>(List.of(fq1, fq2, fq3, fq4)); + List questionsBefore = new ArrayList<>(List.of(fq1, fq2, fq3, fq4)); fs.setId(UUID.randomUUID()); when(fqDb.getFeedbackQuestionsForSession(fs.getId())).thenReturn(questionsBefore); when(fqDb.createFeedbackQuestion(fq5)).thenReturn(fq5); @@ -170,7 +170,7 @@ public void testCreateFeedbackQuestion_oldQuestionNumberSmallerThanNewQuestionNu fq3.setQuestionNumber(2); fq4.setQuestionNumber(3); - ArrayList questionsBefore = new ArrayList<>(List.of(fq1, fq2, fq3, fq4)); + List questionsBefore = new ArrayList<>(List.of(fq1, fq2, fq3, fq4)); fs.setId(UUID.randomUUID()); when(fqDb.getFeedbackQuestionsForSession(fs.getId())).thenReturn(questionsBefore); when(fqDb.createFeedbackQuestion(fq5)).thenReturn(fq5); @@ -275,7 +275,7 @@ public void testGetRecipientsOfQuestion_giverTypeStudents() { } private List createQuestionList(FeedbackSession fs, int numOfQuestions) { - ArrayList questions = new ArrayList<>(); + List questions = new ArrayList<>(); for (int i = 1; i <= numOfQuestions; i++) { FeedbackQuestion fq = getTypicalFeedbackQuestionForSession(fs); fq.setQuestionNumber(i); diff --git a/src/test/java/teammates/sqlui/webapi/BaseActionTest.java b/src/test/java/teammates/sqlui/webapi/BaseActionTest.java index 615f031d1c6b..98e3535fecb7 100644 --- a/src/test/java/teammates/sqlui/webapi/BaseActionTest.java +++ b/src/test/java/teammates/sqlui/webapi/BaseActionTest.java @@ -7,7 +7,7 @@ import java.util.List; import java.util.Map; -import javax.servlet.http.Cookie; +import jakarta.servlet.http.Cookie; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpDelete; diff --git a/src/test/java/teammates/sqlui/webapi/GetFeedbackSessionLogsActionTest.java b/src/test/java/teammates/sqlui/webapi/GetFeedbackSessionLogsActionTest.java new file mode 100644 index 000000000000..73af8c67f3d5 --- /dev/null +++ b/src/test/java/teammates/sqlui/webapi/GetFeedbackSessionLogsActionTest.java @@ -0,0 +1,330 @@ +package teammates.sqlui.webapi; + +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.InstructorPrivileges; +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.util.Const; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.storage.sqlentity.Instructor; +import teammates.storage.sqlentity.Student; +import teammates.ui.output.FeedbackSessionLogData; +import teammates.ui.output.FeedbackSessionLogEntryData; +import teammates.ui.output.FeedbackSessionLogsData; +import teammates.ui.webapi.GetFeedbackSessionLogsAction; +import teammates.ui.webapi.JsonResult; + +/** + * SUT: {@link GetFeedbackSessionLogsAction}. + */ +public class GetFeedbackSessionLogsActionTest extends BaseActionTest { + + private Course course; + + private Student student1; + private Student student2; + + private FeedbackSession fs1; + + private long startTime; + private long endTime; + + private String googleId = "google-id"; + + @Override + String getActionUri() { + return Const.ResourceURIs.SESSION_LOGS; + } + + @Override + String getRequestMethod() { + return GET; + } + + @BeforeMethod + void setUp() { + FeedbackSession fs2; + endTime = Instant.now().toEpochMilli(); + startTime = endTime - (Const.LOGS_RETENTION_PERIOD.toDays() - 1) * 24 * 60 * 60 * 1000; + + course = getTypicalCourse(); + + student1 = getTypicalStudent(); + student1.setEmail("student1@teammates.tmt"); + student1.setTeam(getTypicalTeam()); + + student2 = getTypicalStudent(); + student2.setEmail("student2@teammates.tmt"); + student2.setTeam(getTypicalTeam()); + + fs1 = getTypicalFeedbackSessionForCourse(course); + fs1.setName("fs1"); + fs1.setCreatedAt(Instant.now()); + + fs2 = getTypicalFeedbackSessionForCourse(course); + fs2.setName("fs2"); + fs2.setCreatedAt(Instant.now()); + + when(mockLogic.getCourse(course.getId())).thenReturn(course); + when(mockLogic.getFeedbackSession(fs1.getId())).thenReturn(fs1); + when(mockLogic.getStudent(student1.getId())).thenReturn(student1); + when(mockLogic.getStudent(student2.getId())).thenReturn(student2); + + List feedbackSessions = new ArrayList<>(); + feedbackSessions.add(fs1); + feedbackSessions.add(fs2); + when(mockLogic.getFeedbackSessionsForCourse(course.getId())).thenReturn(feedbackSessions); + + FeedbackSessionLog student1Session1Log1 = new FeedbackSessionLog(student1, fs1, FeedbackSessionLogType.ACCESS, + Instant.ofEpochMilli(startTime)); + FeedbackSessionLog student1Session2Log1 = new FeedbackSessionLog(student1, fs2, FeedbackSessionLogType.ACCESS, + Instant.ofEpochMilli(startTime + 1000)); + FeedbackSessionLog student1Session2Log2 = new FeedbackSessionLog(student1, fs2, + FeedbackSessionLogType.SUBMISSION, Instant.ofEpochMilli(startTime + 2000)); + FeedbackSessionLog student2Session1Log1 = new FeedbackSessionLog(student2, fs1, FeedbackSessionLogType.ACCESS, + Instant.ofEpochMilli(startTime + 3000)); + FeedbackSessionLog student2Session1Log2 = new FeedbackSessionLog(student2, fs1, + FeedbackSessionLogType.SUBMISSION, Instant.ofEpochMilli(startTime + 4000)); + + List allLogsInCourse = new ArrayList<>(); + allLogsInCourse.add(student1Session1Log1); + allLogsInCourse.add(student1Session2Log1); + allLogsInCourse.add(student1Session2Log2); + allLogsInCourse.add(student2Session1Log1); + allLogsInCourse.add(student2Session1Log2); + when(mockLogic.getOrderedFeedbackSessionLogs(course.getId(), null, null, Instant.ofEpochMilli(startTime), + Instant.ofEpochMilli(endTime))).thenReturn(allLogsInCourse); + + List student1Logs = new ArrayList<>(); + student1Logs.add(student1Session1Log1); + student1Logs.add(student1Session2Log1); + student1Logs.add(student1Session2Log2); + when(mockLogic.getOrderedFeedbackSessionLogs(course.getId(), student1.getId(), null, + Instant.ofEpochMilli(startTime), Instant.ofEpochMilli(endTime))).thenReturn(student1Logs); + + List fs1Logs = new ArrayList<>(); + fs1Logs.add(student1Session1Log1); + fs1Logs.add(student2Session1Log1); + fs1Logs.add(student2Session1Log2); + when(mockLogic.getOrderedFeedbackSessionLogs(course.getId(), null, fs1.getId(), + Instant.ofEpochMilli(startTime), Instant.ofEpochMilli(endTime))).thenReturn(fs1Logs); + + List student1Fs1Logs = new ArrayList<>(); + student1Fs1Logs.add(student1Session1Log1); + when(mockLogic.getOrderedFeedbackSessionLogs(course.getId(), student1.getId(), fs1.getId(), + Instant.ofEpochMilli(startTime), Instant.ofEpochMilli(endTime))).thenReturn(student1Fs1Logs); + } + + @Test + protected void testExecute() { + JsonResult actionOutput; + + ______TS("Failure case: not enough parameters"); + verifyHttpParameterFailure( + Const.ParamsNames.COURSE_ID, course.getId()); + + verifyHttpParameterFailure( + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime)); + verifyHttpParameterFailure( + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime)); + + ______TS("Failure case: invalid course id"); + String[] paramsInvalid1 = { + Const.ParamsNames.COURSE_ID, "fake-course-id", + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + verifyEntityNotFound(paramsInvalid1); + + ______TS("Failure case: invalid student id"); + String[] paramsInvalid2 = { + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.STUDENT_SQL_ID, "00000000-0000-0000-0000-000000000000", + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + verifyEntityNotFound(paramsInvalid2); + + ______TS("Failure case: invalid start or end times"); + String[] paramsInvalid3 = { + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, "abc", + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + verifyHttpParameterFailure(paramsInvalid3); + + String[] paramsInvalid4 = { + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, " ", + }; + verifyHttpParameterFailure(paramsInvalid4); + + ______TS("Success case: should group by feedback session"); + String[] paramsSuccessful1 = { + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + + actionOutput = getJsonResult(getAction(paramsSuccessful1)); + + FeedbackSessionLogsData fslData = (FeedbackSessionLogsData) actionOutput.getOutput(); + List fsLogs = fslData.getFeedbackSessionLogs(); + + // Course has 2 feedback sessions + assertEquals(fsLogs.size(), 2); + + List fsLogEntries1 = fsLogs.get(0).getFeedbackSessionLogEntries(); + List fsLogEntries2 = fsLogs.get(1).getFeedbackSessionLogEntries(); + + assertEquals(fsLogEntries1.size(), 3); + assertEquals(fsLogEntries1.get(0).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries1.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries1.get(1).getStudentData().getEmail(), student2.getEmail()); + assertEquals(fsLogEntries1.get(1).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries1.get(2).getStudentData().getEmail(), student2.getEmail()); + assertEquals(fsLogEntries1.get(2).getFeedbackSessionLogType(), FeedbackSessionLogType.SUBMISSION); + + assertEquals(fsLogEntries2.size(), 2); + assertEquals(fsLogEntries2.get(0).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries2.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries2.get(1).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries2.get(1).getFeedbackSessionLogType(), FeedbackSessionLogType.SUBMISSION); + + ______TS("Success case: should accept optional student id"); + String[] paramsSuccessful2 = { + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + actionOutput = getJsonResult(getAction(paramsSuccessful2)); + fslData = (FeedbackSessionLogsData) actionOutput.getOutput(); + fsLogs = fslData.getFeedbackSessionLogs(); + + assertEquals(fsLogs.size(), 2); + + fsLogEntries1 = fsLogs.get(0).getFeedbackSessionLogEntries(); + fsLogEntries2 = fsLogs.get(1).getFeedbackSessionLogEntries(); + + assertEquals(fsLogEntries1.size(), 1); + assertEquals(fsLogEntries1.get(0).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries1.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + + assertEquals(fsLogEntries2.size(), 2); + assertEquals(fsLogEntries2.get(0).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries2.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries2.get(1).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries2.get(1).getFeedbackSessionLogType(), FeedbackSessionLogType.SUBMISSION); + + ______TS("Success case: should accept optional feedback session"); + String[] paramsSuccessful3 = { + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.FEEDBACK_SESSION_ID, fs1.getId().toString(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + actionOutput = getJsonResult(getAction(paramsSuccessful3)); + fslData = (FeedbackSessionLogsData) actionOutput.getOutput(); + fsLogs = fslData.getFeedbackSessionLogs(); + + assertEquals(fsLogs.size(), 2); + assertEquals(fsLogs.get(1).getFeedbackSessionLogEntries().size(), 0); + + fsLogEntries1 = fsLogs.get(0).getFeedbackSessionLogEntries(); + + assertEquals(fsLogEntries1.size(), 3); + assertEquals(fsLogEntries1.get(0).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries1.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries1.get(1).getStudentData().getEmail(), student2.getEmail()); + assertEquals(fsLogEntries1.get(1).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries1.get(2).getStudentData().getEmail(), student2.getEmail()); + assertEquals(fsLogEntries1.get(2).getFeedbackSessionLogType(), FeedbackSessionLogType.SUBMISSION); + + ______TS("Success case: should accept all optional params"); + String[] paramsSuccessful4 = { + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), + Const.ParamsNames.FEEDBACK_SESSION_ID, fs1.getId().toString(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + actionOutput = getJsonResult(getAction(paramsSuccessful4)); + fslData = (FeedbackSessionLogsData) actionOutput.getOutput(); + fsLogs = fslData.getFeedbackSessionLogs(); + + assertEquals(fsLogs.size(), 2); + assertEquals(fsLogs.get(1).getFeedbackSessionLogEntries().size(), 0); + + fsLogEntries1 = fsLogs.get(0).getFeedbackSessionLogEntries(); + + assertEquals(fsLogEntries1.size(), 1); + assertEquals(fsLogEntries1.get(0).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries1.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + + // TODO: if we restrict the range from start to end time, it should be tested + // here as well + } + + @Test + void testSpecificAccessControl_instructorWithInvalidPermission_cannotAccess() { + + Instructor instructor = new Instructor(course, "name", "instructoremail@tm.tmt", + false, "", null, new InstructorPrivileges()); + + loginAsInstructor(googleId); + when(mockLogic.getCourse(course.getId())).thenReturn(course); + when(mockLogic.getInstructorByGoogleId(course.getId(), googleId)).thenReturn(instructor); + + String[] params = { + Const.ParamsNames.COURSE_ID, course.getId(), + }; + + verifyCannotAccess(params); + } + + @Test + void testSpecificAccessControl_instructorWithPermission_canAccess() { + InstructorPrivileges instructorPrivileges = new InstructorPrivileges(); + instructorPrivileges.updatePrivilege(Const.InstructorPermissions.CAN_MODIFY_SESSION, true); + instructorPrivileges.updatePrivilege(Const.InstructorPermissions.CAN_MODIFY_STUDENT, true); + instructorPrivileges.updatePrivilege(Const.InstructorPermissions.CAN_MODIFY_INSTRUCTOR, true); + Instructor instructor = new Instructor(course, "name", "instructoremail@tm.tmt", + false, "", null, instructorPrivileges); + + loginAsInstructor(googleId); + when(mockLogic.getCourse(course.getId())).thenReturn(course); + when(mockLogic.getInstructorByGoogleId(course.getId(), googleId)).thenReturn(instructor); + + String[] params = { + Const.ParamsNames.COURSE_ID, course.getId(), + }; + + verifyCanAccess(params); + } + + @Test + void testSpecificAccessControl_notInstructor_cannotAccess() { + String[] params = { + Const.ParamsNames.COURSE_ID, course.getId(), + }; + loginAsStudent(googleId); + verifyCannotAccess(params); + + logoutUser(); + verifyCannotAccess(params); + } +} diff --git a/src/test/java/teammates/sqlui/webapi/UpdateFeedbackSessionLogsActionTest.java b/src/test/java/teammates/sqlui/webapi/UpdateFeedbackSessionLogsActionTest.java new file mode 100644 index 000000000000..ebd2a467001a --- /dev/null +++ b/src/test/java/teammates/sqlui/webapi/UpdateFeedbackSessionLogsActionTest.java @@ -0,0 +1,233 @@ +package teammates.sqlui.webapi; + +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.FeedbackSessionLogEntry; +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.exception.EntityAlreadyExistsException; +import teammates.common.exception.InvalidParametersException; +import teammates.common.util.Const; +import teammates.common.util.TimeHelper; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.storage.sqlentity.Student; +import teammates.ui.webapi.UpdateFeedbackSessionLogsAction; + +/** + * SUT: {@link UpdateFeedbackSessionLogsAction}. + */ +public class UpdateFeedbackSessionLogsActionTest + extends BaseActionTest { + + static final long COLLECTION_TIME_PERIOD = Const.STUDENT_ACTIVITY_LOGS_UPDATE_INTERVAL.toMinutes(); + static final long SPAM_FILTER = Const.STUDENT_ACTIVITY_LOGS_FILTER_WINDOW.toMillis(); + + Student student1; + Student student2; + + Course course1; + Course course2; + + FeedbackSession session1InCourse1; + FeedbackSession session2InCourse1; + FeedbackSession session1InCourse2; + + Instant endTime; + Instant startTime; + + @Override + protected String getActionUri() { + return Const.CronJobURIs.AUTOMATED_FEEDBACK_SESSION_LOGS_PROCESSING; + } + + @Override + String getRequestMethod() { + return GET; + } + + @BeforeMethod + void setUp() { + endTime = TimeHelper.getInstantNearestQuarterHourBefore(Instant.now()); + startTime = endTime.minus(COLLECTION_TIME_PERIOD, ChronoUnit.MINUTES); + + course1 = getTypicalCourse(); + course1.setId("course1"); + + course2 = getTypicalCourse(); + course2.setId("course2"); + + student1 = getTypicalStudent(); + student1.setEmail("student1@teammates.tmt"); + student1.setId(UUID.randomUUID()); + + student2 = getTypicalStudent(); + student2.setEmail("student2@teammates.tmt"); + student2.setId(UUID.randomUUID()); + + session1InCourse1 = getTypicalFeedbackSessionForCourse(course1); + session1InCourse1.setName("session1"); + session1InCourse1.setId(UUID.randomUUID()); + + session2InCourse1 = getTypicalFeedbackSessionForCourse(course1); + session2InCourse1.setName("session2"); + session2InCourse1.setId(UUID.randomUUID()); + + session1InCourse2 = getTypicalFeedbackSessionForCourse(course2); + session1InCourse2.setName("session1"); + session1InCourse2.setId(UUID.randomUUID()); + + reset(mockLogic); + + when(mockLogic.getStudentReference(student1.getId())).thenReturn(student1); + when(mockLogic.getStudentReference(student2.getId())).thenReturn(student2); + + when(mockLogic.getFeedbackSessionReference(session1InCourse1.getId())).thenReturn(session1InCourse1); + when(mockLogic.getFeedbackSessionReference(session2InCourse1.getId())).thenReturn(session2InCourse1); + when(mockLogic.getFeedbackSessionReference(session1InCourse2.getId())).thenReturn(session1InCourse2); + + mockLogsProcessor.getOrderedFeedbackSessionLogs("", "", 0, 0, "").clear(); + } + + @Test + public void testExecute_noRecentLogs_noLogsCreated() + throws EntityAlreadyExistsException, InvalidParametersException { + UpdateFeedbackSessionLogsAction action = getAction(); + action.execute(); + + verify(mockLogic).createFeedbackSessionLogs(argThat(filteredLogs -> filteredLogs.isEmpty())); + } + + @Test + public void testExecute_recentLogsNoSpam_allLogsCreated() + throws EntityAlreadyExistsException, InvalidParametersException { + // Different Types + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(100).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.SUBMISSION.getLabel(), startTime.plusSeconds(100).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.VIEW_RESULT.getLabel(), startTime.plusSeconds(100).toEpochMilli()); + + // Different feedback sessions + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(200).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session2InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(200).toEpochMilli()); + + // Different Student + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(300).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student2.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(300).toEpochMilli()); + + // Different course + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(400).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course2.getId(), student1.getId(), session1InCourse2.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(400).toEpochMilli()); + + // Gap is larger than spam filter + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusMillis(SPAM_FILTER + 1).toEpochMilli()); + + UpdateFeedbackSessionLogsAction action = getAction(); + action.execute(); + + // method returns all logs regardless of params + List expected = mockLogsProcessor.getOrderedFeedbackSessionLogs("", "", 0, 0, ""); + + verify(mockLogic).createFeedbackSessionLogs(argThat(filteredLogs -> isEqual(expected, filteredLogs))); + } + + @Test + public void testExecute_recentLogsWithSpam_someLogsCreated() + throws EntityAlreadyExistsException, InvalidParametersException { + // Gap is smaller than spam filter + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusMillis(SPAM_FILTER - 2).toEpochMilli()); + + // Filters multiple logs within one spam window + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusMillis(SPAM_FILTER - 1).toEpochMilli()); + + // Correctly adds new log after filtering + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusMillis(SPAM_FILTER + 1).toEpochMilli()); + + // Filters out spam in the new window + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusMillis(SPAM_FILTER + 2).toEpochMilli()); + + UpdateFeedbackSessionLogsAction action = getAction(); + action.execute(); + + List expected = new ArrayList<>(); + expected.add(new FeedbackSessionLogEntry(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.toEpochMilli())); + expected.add(new FeedbackSessionLogEntry(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusMillis(SPAM_FILTER + 1).toEpochMilli())); + + verify(mockLogic).createFeedbackSessionLogs(argThat(filteredLogs -> isEqual(expected, filteredLogs))); + } + + @Test + public void testSpecificAccessControl_isAdmin_canAccess() { + loginAsAdmin(); + verifyCanAccess(); + } + + @Test + public void testSpecificAccessControl_isInstructor_cannotAccess() { + loginAsInstructor("user-id"); + verifyCannotAccess(); + } + + @Test + public void testSpecificAccessControl_isStudent_cannotAccess() { + loginAsStudent("user-id"); + verifyCannotAccess(); + } + + @Test + public void testSpecificAccessControl_loggedOut_cannotAccess() { + logoutUser(); + verifyCannotAccess(); + } + + private Boolean isEqual(List expected, List actual) { + + assertEquals(expected.size(), actual.size()); + + for (int i = 0; i < expected.size(); i++) { + FeedbackSessionLogEntry expectedEntry = expected.get(i); + FeedbackSessionLog actualLog = actual.get(i); + + assertEquals(expectedEntry.getStudentId(), actualLog.getStudent().getId()); + + assertEquals(expectedEntry.getFeedbackSessionId(), actualLog.getFeedbackSession().getId()); + + assertEquals(expectedEntry.getFeedbackSessionLogType(), actualLog.getFeedbackSessionLogType().getLabel()); + + assertEquals(expectedEntry.getTimestamp(), actualLog.getTimestamp().toEpochMilli()); + } + + return true; + } +} diff --git a/src/test/java/teammates/storage/api/NotificationsDbTest.java b/src/test/java/teammates/storage/api/NotificationsDbTest.java index 282254e98bec..c90bb4091ec2 100644 --- a/src/test/java/teammates/storage/api/NotificationsDbTest.java +++ b/src/test/java/teammates/storage/api/NotificationsDbTest.java @@ -23,6 +23,7 @@ public class NotificationsDbTest extends BaseTestCaseWithLocalDatabaseAccess { private final NotificationsDb notificationsDb = NotificationsDb.inst(); + @SuppressWarnings("PMD.FinalFieldCouldBeStatic") private final Map typicalNotifications = getTypicalDataBundle().notifications; @BeforeMethod diff --git a/src/test/java/teammates/storage/sqlapi/AccountRequestsDbTest.java b/src/test/java/teammates/storage/sqlapi/AccountRequestsDbTest.java index ef31306aef99..1044cb034c4f 100644 --- a/src/test/java/teammates/storage/sqlapi/AccountRequestsDbTest.java +++ b/src/test/java/teammates/storage/sqlapi/AccountRequestsDbTest.java @@ -1,6 +1,5 @@ package teammates.storage.sqlapi; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; @@ -10,13 +9,14 @@ import static org.mockito.Mockito.verify; import java.util.List; +import java.util.UUID; import org.mockito.MockedStatic; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import teammates.common.exception.EntityAlreadyExistsException; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; import teammates.common.exception.SearchServiceException; @@ -49,32 +49,38 @@ public void teardownMethod() { } @Test - public void testCreateAccountRequest_accountRequestDoesNotExist_success() - throws InvalidParametersException, EntityAlreadyExistsException { - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); - doReturn(null).when(accountRequestDb).getAccountRequest(anyString(), anyString()); - + public void testCreateAccountRequest_typicalCase_success() throws InvalidParametersException { + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); accountRequestDb.createAccountRequest(accountRequest); mockHibernateUtil.verify(() -> HibernateUtil.persist(accountRequest)); } @Test - public void testCreateAccountRequest_accountRequestAlreadyExists_throwsEntityAlreadyExistsException() { - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); - doReturn(new AccountRequest("test@gmail.com", "name", "institute")) - .when(accountRequestDb).getAccountRequest(anyString(), anyString()); - - EntityAlreadyExistsException ex = assertThrows(EntityAlreadyExistsException.class, - () -> accountRequestDb.createAccountRequest(accountRequest)); + public void testGetAccountRequest_nonExistentAccountRequest_returnsNull() { + UUID id = UUID.randomUUID(); + mockHibernateUtil.when(() -> HibernateUtil.get(AccountRequest.class, id)).thenReturn(null); + AccountRequest actualAccountRequest = accountRequestDb.getAccountRequest(id); + mockHibernateUtil.verify(() -> HibernateUtil.get(AccountRequest.class, id)); + assertNull(actualAccountRequest); + } - assertEquals(ex.getMessage(), "Trying to create an entity that exists: " + accountRequest.toString()); - mockHibernateUtil.verify(() -> HibernateUtil.persist(accountRequest), never()); + @Test + public void testGetAccountRequest_existingAccountRequest_getsSuccessfully() { + AccountRequest expectedAccountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); + UUID id = expectedAccountRequest.getId(); + mockHibernateUtil.when(() -> HibernateUtil.get(AccountRequest.class, id)).thenReturn(expectedAccountRequest); + AccountRequest actualAccountRequest = accountRequestDb.getAccountRequest(id); + mockHibernateUtil.verify(() -> HibernateUtil.get(AccountRequest.class, id)); + assertEquals(expectedAccountRequest, actualAccountRequest); } @Test public void testUpdateAccountRequest_invalidEmail_throwsInvalidParametersException() { - AccountRequest accountRequestWithInvalidEmail = new AccountRequest("testgmail.com", "name", "institute"); + AccountRequest accountRequestWithInvalidEmail = + new AccountRequest("testgmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); assertThrows(InvalidParametersException.class, () -> accountRequestDb.updateAccountRequest(accountRequestWithInvalidEmail)); @@ -84,8 +90,9 @@ public void testUpdateAccountRequest_invalidEmail_throwsInvalidParametersExcepti @Test public void testUpdateAccountRequest_accountRequestDoesNotExist_throwsEntityDoesNotExistException() { - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); - doReturn(null).when(accountRequestDb).getAccountRequest(anyString(), anyString()); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); + doReturn(null).when(accountRequestDb).getAccountRequest(accountRequest.getId()); assertThrows(EntityDoesNotExistException.class, () -> accountRequestDb.updateAccountRequest(accountRequest)); @@ -95,8 +102,9 @@ public void testUpdateAccountRequest_accountRequestDoesNotExist_throwsEntityDoes @Test public void testUpdateAccountRequest_success() throws InvalidParametersException, EntityDoesNotExistException { - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); - doReturn(accountRequest).when(accountRequestDb).getAccountRequest(anyString(), anyString()); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); + doReturn(accountRequest).when(accountRequestDb).getAccountRequest(accountRequest.getId()); accountRequestDb.updateAccountRequest(accountRequest); @@ -105,7 +113,8 @@ public void testUpdateAccountRequest_success() throws InvalidParametersException @Test public void testDeleteAccountRequest_success() { - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); accountRequestDb.deleteAccountRequest(accountRequest); diff --git a/src/test/java/teammates/storage/sqlapi/FeedbackSessionLogsDbTest.java b/src/test/java/teammates/storage/sqlapi/FeedbackSessionLogsDbTest.java new file mode 100644 index 000000000000..ee6bf29b1ff3 --- /dev/null +++ b/src/test/java/teammates/storage/sqlapi/FeedbackSessionLogsDbTest.java @@ -0,0 +1,46 @@ +package teammates.storage.sqlapi; + +import static org.mockito.Mockito.mockStatic; + +import java.time.Instant; + +import org.mockito.MockedStatic; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.util.HibernateUtil; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.test.BaseTestCase; + +/** + * SUT: {@code FeedbackSessionLogsDb}. + */ +public class FeedbackSessionLogsDbTest extends BaseTestCase { + + private FeedbackSessionLogsDb feedbackSessionLogsDb = FeedbackSessionLogsDb.inst(); + + private MockedStatic mockHibernateUtil; + + @BeforeMethod + public void setUpMethod() { + mockHibernateUtil = mockStatic(HibernateUtil.class); + } + + @AfterMethod + public void teardownMethod() { + mockHibernateUtil.close(); + } + + @Test + public void testCreateFeedbackSessionLog_success() { + + FeedbackSessionLog logToAdd = new FeedbackSessionLog(getTypicalStudent(), + getTypicalFeedbackSessionForCourse(getTypicalCourse()), FeedbackSessionLogType.ACCESS, + Instant.parse("2011-01-01T00:00:00Z")); + feedbackSessionLogsDb.createFeedbackSessionLog(logToAdd); + + mockHibernateUtil.verify(() -> HibernateUtil.persist(logToAdd)); + } +} diff --git a/src/test/java/teammates/test/AbstractBackDoor.java b/src/test/java/teammates/test/AbstractBackDoor.java index 16c62c5e3edb..843195803254 100644 --- a/src/test/java/teammates/test/AbstractBackDoor.java +++ b/src/test/java/teammates/test/AbstractBackDoor.java @@ -646,7 +646,7 @@ private Map convertDeadlinesToInstant(Map deadlin /** * Get soft deleted feedback session from database. */ - public FeedbackSessionAttributes getSoftDeletedSession(String feedbackSessionName, String instructorId) { + public FeedbackSessionData getSoftDeletedSessionData(String feedbackSessionName, String instructorId) { Map params = new HashMap<>(); params.put(Const.ParamsNames.ENTITY_TYPE, Const.EntityType.INSTRUCTOR); params.put(Const.ParamsNames.IS_IN_RECYCLE_BIN, "true"); @@ -657,12 +657,18 @@ public FeedbackSessionAttributes getSoftDeletedSession(String feedbackSessionNam } FeedbackSessionsData sessionsData = JsonUtils.fromJson(response.responseBody, FeedbackSessionsData.class); - FeedbackSessionData feedbackSession = sessionsData.getFeedbackSessions() + return sessionsData.getFeedbackSessions() .stream() .filter(fs -> fs.getFeedbackSessionName().equals(feedbackSessionName)) .findFirst() .orElse(null); + } + /** + * Get soft deleted feedback session from database. + */ + public FeedbackSessionAttributes getSoftDeletedSession(String feedbackSessionName, String instructorId) { + FeedbackSessionData feedbackSession = getSoftDeletedSessionData(feedbackSessionName, instructorId); if (feedbackSession == null) { return null; } @@ -832,10 +838,9 @@ public void deleteCourse(String courseId) { /** * Gets an account request from the database. */ - public AccountRequestAttributes getAccountRequest(String email, String institute) { + public AccountRequestAttributes getAccountRequest(UUID id) { Map params = new HashMap<>(); - params.put(Const.ParamsNames.INSTRUCTOR_EMAIL, email); - params.put(Const.ParamsNames.INSTRUCTOR_INSTITUTION, institute); + params.put(Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()); ResponseBodyAndCode response = executeGetRequest(Const.ResourceURIs.ACCOUNT_REQUEST, params); if (response.responseCode == HttpStatus.SC_NOT_FOUND) { @@ -852,10 +857,9 @@ public AccountRequestAttributes getAccountRequest(String email, String institute /** * Gets registration key of an account request from the database. */ - public String getRegKeyForAccountRequest(String email, String institute) { + public String getRegKeyForAccountRequest(UUID id) { Map params = new HashMap<>(); - params.put(Const.ParamsNames.INSTRUCTOR_EMAIL, email); - params.put(Const.ParamsNames.INSTRUCTOR_INSTITUTION, institute); + params.put(Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()); ResponseBodyAndCode response = executeGetRequest(Const.ResourceURIs.ACCOUNT_REQUEST, params); if (response.responseCode == HttpStatus.SC_NOT_FOUND) { @@ -868,10 +872,9 @@ public String getRegKeyForAccountRequest(String email, String institute) { /** * Deletes an account request from the database. */ - public void deleteAccountRequest(String email, String institute) { + public void deleteAccountRequest(UUID id) { Map params = new HashMap<>(); - params.put(Const.ParamsNames.INSTRUCTOR_EMAIL, email); - params.put(Const.ParamsNames.INSTRUCTOR_INSTITUTION, institute); + params.put(Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()); executeDeleteRequest(Const.ResourceURIs.ACCOUNT_REQUEST, params); } diff --git a/src/test/java/teammates/test/AssertHelper.java b/src/test/java/teammates/test/AssertHelper.java index 384388a753ea..bd96ccd8dd59 100644 --- a/src/test/java/teammates/test/AssertHelper.java +++ b/src/test/java/teammates/test/AssertHelper.java @@ -1,7 +1,7 @@ package teammates.test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import java.time.Instant; import java.util.ArrayList; diff --git a/src/test/java/teammates/test/BaseTestCase.java b/src/test/java/teammates/test/BaseTestCase.java index a7744061d552..91ec35f2ff7d 100644 --- a/src/test/java/teammates/test/BaseTestCase.java +++ b/src/test/java/teammates/test/BaseTestCase.java @@ -9,10 +9,12 @@ import java.util.Map; import java.util.UUID; -import org.junit.Assert; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.function.Executable; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.datatransfer.DataBundle; import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.InstructorPermissionRole; @@ -29,6 +31,7 @@ import teammates.common.util.TimeHelperExtension; import teammates.sqllogic.core.DataBundleLogic; import teammates.storage.sqlentity.Account; +import teammates.storage.sqlentity.AccountRequest; import teammates.storage.sqlentity.Course; import teammates.storage.sqlentity.FeedbackQuestion; import teammates.storage.sqlentity.FeedbackResponse; @@ -43,6 +46,7 @@ /** * Base class for all test cases. */ +@SuppressWarnings("PMD.TestClassWithoutTestCases") public class BaseTestCase { /** @@ -183,8 +187,8 @@ protected FeedbackSession getTypicalFeedbackSessionForCourse(Course course) { protected FeedbackQuestion getTypicalFeedbackQuestionForSession(FeedbackSession session) { return FeedbackQuestion.makeQuestion(session, 1, "test-description", - FeedbackParticipantType.SELF, FeedbackParticipantType.SELF, 1, new ArrayList(), - new ArrayList(), new ArrayList(), + FeedbackParticipantType.SELF, FeedbackParticipantType.SELF, 1, new ArrayList<>(), + new ArrayList<>(), new ArrayList<>(), new FeedbackTextQuestionDetails("test question text")); } @@ -206,6 +210,11 @@ protected FeedbackResponseComment getTypicalResponseComment(Long id) { return comment; } + protected AccountRequest getTypicalAccountRequest() { + return new AccountRequest("valid@test.com", "Test Name", "TEAMMATES Test Institute 1, Test Country", + AccountRequestStatus.PENDING, ""); + } + /** * Populates the feedback question and response IDs within the data bundle. * @@ -277,127 +286,83 @@ protected static String getPopulatedEmptyStringErrorMessage(String messageTempla */ protected static void assertTrue(boolean condition) { - Assert.assertTrue(condition); + Assertions.assertTrue(condition); } protected static void assertTrue(String message, boolean condition) { - Assert.assertTrue(message, condition); + Assertions.assertTrue(condition, message); } protected static void assertFalse(boolean condition) { - Assert.assertFalse(condition); + Assertions.assertFalse(condition); } protected static void assertFalse(String message, boolean condition) { - Assert.assertFalse(message, condition); + Assertions.assertFalse(condition, message); } protected static void assertEquals(int expected, int actual) { - Assert.assertEquals(expected, actual); + Assertions.assertEquals(expected, actual); } protected static void assertEquals(String message, int expected, int actual) { - Assert.assertEquals(message, expected, actual); + Assertions.assertEquals(expected, actual, message); } protected static void assertEquals(long expected, long actual) { - Assert.assertEquals(expected, actual); + Assertions.assertEquals(expected, actual); } protected static void assertEquals(double expected, double actual, double delta) { - Assert.assertEquals(expected, actual, delta); + Assertions.assertEquals(expected, actual, delta); } protected static void assertEquals(Object expected, Object actual) { - Assert.assertEquals(expected, actual); + Assertions.assertEquals(expected, actual); } protected static void assertEquals(String message, Object expected, Object actual) { - Assert.assertEquals(message, expected, actual); + Assertions.assertEquals(expected, actual, message); } protected static void assertArrayEquals(byte[] expected, byte[] actual) { - Assert.assertArrayEquals(expected, actual); + Assertions.assertArrayEquals(expected, actual); } protected static void assertNotEquals(Object first, Object second) { - Assert.assertNotEquals(first, second); + Assertions.assertNotEquals(first, second); } protected static void assertSame(Object expected, Object actual) { - Assert.assertSame(expected, actual); + Assertions.assertSame(expected, actual); } protected static void assertNotSame(Object unexpected, Object actual) { - Assert.assertNotSame(unexpected, actual); + Assertions.assertNotSame(unexpected, actual); } protected static void assertNull(Object object) { - Assert.assertNull(object); + Assertions.assertNull(object); } protected static void assertNull(String message, Object object) { - Assert.assertNull(message, object); + Assertions.assertNull(object, message); } protected static void assertNotNull(Object object) { - Assert.assertNotNull(object); + Assertions.assertNotNull(object); } protected static void assertNotNull(String message, Object object) { - Assert.assertNotNull(message, object); + Assertions.assertNotNull(object, message); } protected static void fail(String message) { - Assert.fail(message); + Assertions.fail(message); } - // This method is adapted from JUnit 5's assertThrows. - // Once we upgrade to JUnit 5, their built-in method shall be used instead. - @SuppressWarnings({ - "unchecked", - "PMD.AvoidCatchingThrowable", // As per reference method's specification - }) protected static T assertThrows(Class expectedType, Executable executable) { - try { - executable.execute(); - } catch (Throwable actualException) { - if (expectedType.isInstance(actualException)) { - return (T) actualException; - } else { - String message = String.format("Expected %s to be thrown, but %s was instead thrown.", - getCanonicalName(expectedType), getCanonicalName(actualException.getClass())); - throw new AssertionError(message, actualException); - } - } - - String message = String.format("Expected %s to be thrown, but nothing was thrown.", getCanonicalName(expectedType)); - throw new AssertionError(message); - } - - private static String getCanonicalName(Class clazz) { - String canonicalName = clazz.getCanonicalName(); - return canonicalName == null ? clazz.getName() : canonicalName; + return Assertions.assertThrows(expectedType, executable); } - /** - * {@code Executable} is a functional interface that can be used to - * implement any generic block of code that potentially throws a - * {@link Throwable}. - * - *

    The {@code Executable} interface is similar to {@link Runnable}, - * except that an {@code Executable} can throw any kind of exception. - */ - // This interface is adapted from JUnit 5's Executable interface. - // Once we upgrade to JUnit 5, this interface shall no longer be necessary. - public interface Executable { - - /** - * Executes a block of code, potentially throwing a {@link Throwable}. - */ - // CHECKSTYLE.OFF:IllegalThrows - void execute() throws Throwable; - // CHECKSTYLE.ON:IllegalThrows - - } } diff --git a/src/test/java/teammates/test/BaseTestCaseWithSqlDatabaseAccess.java b/src/test/java/teammates/test/BaseTestCaseWithSqlDatabaseAccess.java index dddf3288a93e..c56634288005 100644 --- a/src/test/java/teammates/test/BaseTestCaseWithSqlDatabaseAccess.java +++ b/src/test/java/teammates/test/BaseTestCaseWithSqlDatabaseAccess.java @@ -222,6 +222,8 @@ private ApiOutput getEntity(BaseEntity entity) { return getStudent((Student) entity); } else if (entity instanceof FeedbackQuestion) { return getFeedbackQuestion((FeedbackQuestion) entity); + } else if (entity instanceof FeedbackSession) { + return getFeedbackSession((FeedbackSession) entity); } else if (entity instanceof FeedbackResponse) { return getFeedbackResponse((FeedbackResponse) entity); } else { @@ -231,6 +233,8 @@ private ApiOutput getEntity(BaseEntity entity) { protected abstract FeedbackQuestionData getFeedbackQuestion(FeedbackQuestion fq); + protected abstract FeedbackSessionData getFeedbackSession(FeedbackSession fq); + protected abstract FeedbackResponseData getFeedbackResponse(FeedbackResponse fq); protected abstract StudentData getStudent(Student student); diff --git a/src/test/java/teammates/test/EmailChecker.java b/src/test/java/teammates/test/EmailChecker.java index 41b6f81cb596..e429f714af70 100644 --- a/src/test/java/teammates/test/EmailChecker.java +++ b/src/test/java/teammates/test/EmailChecker.java @@ -1,6 +1,6 @@ package teammates.test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; diff --git a/src/test/java/teammates/test/MockFilterChain.java b/src/test/java/teammates/test/MockFilterChain.java index 6a4855868001..2919610aa537 100644 --- a/src/test/java/teammates/test/MockFilterChain.java +++ b/src/test/java/teammates/test/MockFilterChain.java @@ -1,8 +1,8 @@ package teammates.test; -import javax.servlet.FilterChain; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; /** * Mocks {@link FilterChain} for testing purpose. diff --git a/src/test/java/teammates/test/MockHttpServletRequest.java b/src/test/java/teammates/test/MockHttpServletRequest.java index d7809c97f5c6..e66396d1c5d8 100644 --- a/src/test/java/teammates/test/MockHttpServletRequest.java +++ b/src/test/java/teammates/test/MockHttpServletRequest.java @@ -13,20 +13,20 @@ import java.util.Locale; import java.util.Map; -import javax.servlet.AsyncContext; -import javax.servlet.DispatcherType; -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletContext; -import javax.servlet.ServletInputStream; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import javax.servlet.http.HttpSessionContext; -import javax.servlet.http.HttpUpgradeHandler; -import javax.servlet.http.Part; +import jakarta.servlet.AsyncContext; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSessionContext; +import jakarta.servlet.http.HttpUpgradeHandler; +import jakarta.servlet.http.Part; import teammates.common.util.Const; diff --git a/src/test/java/teammates/test/MockHttpServletResponse.java b/src/test/java/teammates/test/MockHttpServletResponse.java index a95593a7d335..508d584e4070 100644 --- a/src/test/java/teammates/test/MockHttpServletResponse.java +++ b/src/test/java/teammates/test/MockHttpServletResponse.java @@ -7,9 +7,9 @@ import java.util.List; import java.util.Locale; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus; diff --git a/src/test/java/teammates/test/TestProperties.java b/src/test/java/teammates/test/TestProperties.java index c9124580b67c..809bcd011e6c 100644 --- a/src/test/java/teammates/test/TestProperties.java +++ b/src/test/java/teammates/test/TestProperties.java @@ -11,6 +11,7 @@ /** * Settings for component tests. */ +@SuppressWarnings("PMD.TestClassWithoutTestCases") public final class TestProperties { /** The directory where HTML files for testing email contents are stored. */ diff --git a/src/test/java/teammates/ui/request/AccountRequestRejectionRequestTest.java b/src/test/java/teammates/ui/request/AccountRequestRejectionRequestTest.java new file mode 100644 index 000000000000..412bfcf1d878 --- /dev/null +++ b/src/test/java/teammates/ui/request/AccountRequestRejectionRequestTest.java @@ -0,0 +1,51 @@ +package teammates.ui.request; + +import org.testng.annotations.Test; + +import teammates.test.BaseTestCase; + +/** + * SUT: {@link AccountRequestRejectionRequest}. + */ +public class AccountRequestRejectionRequestTest extends BaseTestCase { + + private static final String TYPICAL_TITLE = "We are Unable to Create an Account for you"; + private static final String TYPICAL_BODY = new StringBuilder() + .append("

    Hi, Example

    \n") + .append("

    Thanks for your interest in using TEAMMATES. ") + .append("We are unable to create a TEAMMATES instructor account for you.

    \n\n") + .append("

    \n") + .append(" Reason: The email address you provided ") + .append("is not an 'official' email address provided by your institution.
    \n") + .append(" Remedy: ") + .append("Please re-submit an account request with your 'official' institution email address.\n") + .append("

    \n\n") + .append("

    If you need further clarification or would like to appeal this decision, ") + .append("please feel free to contact us at teammates@comp.nus.edu.sg.

    \n") + .append("

    Regards,
    TEAMMATES Team.

    \n") + .toString(); + + @Test + public void testValidate_withNonNullBodyAndNonNullTitle_shouldPass() throws Exception { + AccountRequestRejectionRequest request = new AccountRequestRejectionRequest(TYPICAL_TITLE, TYPICAL_BODY); + request.validate(); + } + + @Test + public void testValidate_withNullBodyAndNullTitle_shouldPass() throws Exception { + AccountRequestRejectionRequest request = new AccountRequestRejectionRequest(null, null); + request.validate(); + } + + @Test + public void testValidate_withNonNullBodyAndNullTitle_shouldFail() { + AccountRequestRejectionRequest request = new AccountRequestRejectionRequest(null, TYPICAL_BODY); + assertThrows(InvalidHttpRequestBodyException.class, request::validate); + } + + @Test + public void testValidate_withNullBodyAndNonNullTitle_shouldFail() { + AccountRequestRejectionRequest request = new AccountRequestRejectionRequest(TYPICAL_TITLE, null); + assertThrows(InvalidHttpRequestBodyException.class, request::validate); + } +} diff --git a/src/test/java/teammates/ui/webapi/BaseActionTest.java b/src/test/java/teammates/ui/webapi/BaseActionTest.java index a1d87f563c0b..826eb97d4909 100644 --- a/src/test/java/teammates/ui/webapi/BaseActionTest.java +++ b/src/test/java/teammates/ui/webapi/BaseActionTest.java @@ -5,7 +5,7 @@ import java.util.List; import java.util.Map; -import javax.servlet.http.Cookie; +import jakarta.servlet.http.Cookie; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpDelete; diff --git a/src/test/java/teammates/ui/webapi/CreateAccountRequestActionTest.java b/src/test/java/teammates/ui/webapi/CreateAccountRequestActionTest.java index 1fb58f95c6b9..2e1cfa96b056 100644 --- a/src/test/java/teammates/ui/webapi/CreateAccountRequestActionTest.java +++ b/src/test/java/teammates/ui/webapi/CreateAccountRequestActionTest.java @@ -4,9 +4,7 @@ import teammates.common.datatransfer.attributes.AccountRequestAttributes; import teammates.common.util.Const; -import teammates.common.util.EmailType; -import teammates.common.util.EmailWrapper; -import teammates.ui.output.JoinLinkData; +import teammates.ui.output.AccountRequestData; import teammates.ui.request.AccountCreateRequest; import teammates.ui.request.InvalidHttpRequestBodyException; @@ -64,45 +62,23 @@ protected void testExecute() { assertEquals(institute, accountRequestAttributes.getInstitute()); assertNotNull(accountRequestAttributes.getRegistrationKey()); - String joinLink = accountRequestAttributes.getRegistrationUrl(); - JoinLinkData output = (JoinLinkData) r.getOutput(); - assertEquals(joinLink, output.getJoinLink()); + String registrationKey = accountRequestAttributes.getRegistrationKey(); + AccountRequestData output = (AccountRequestData) r.getOutput(); + assertEquals(registrationKey, output.getRegistrationKey()); - verifyNumberOfEmailsSent(1); + verifyNoEmailsSent(); verifySpecifiedTasksAdded(Const.TaskQueue.SEARCH_INDEXING_QUEUE_NAME, 1); - EmailWrapper emailSent = mockEmailSender.getEmailsSent().get(0); - assertEquals(String.format(EmailType.NEW_INSTRUCTOR_ACCOUNT.getSubject(), name), - emailSent.getSubject()); - assertEquals(email, emailSent.getRecipient()); - assertTrue(emailSent.getContent().contains(joinLink)); - - ______TS("Account request already exists: instructor unregistered, email sent again"); + ______TS("Account request already exists: instructor unregistered"); a = getAction(req); r = getJsonResult(a); - output = (JoinLinkData) r.getOutput(); - assertEquals(joinLink, output.getJoinLink()); + output = (AccountRequestData) r.getOutput(); + assertEquals(registrationKey, output.getRegistrationKey()); - verifyNumberOfEmailsSent(1); + verifyNoEmailsSent(); verifyNoTasksAdded(); // Account request not added to search indexing queue - emailSent = mockEmailSender.getEmailsSent().get(0); - assertEquals(String.format(EmailType.NEW_INSTRUCTOR_ACCOUNT.getSubject(), name), - emailSent.getSubject()); - assertEquals(email, emailSent.getRecipient()); - assertTrue(emailSent.getContent().contains(joinLink)); - - ______TS("Account request already exists: instructor registered, InvalidOperationException thrown"); - - accountRequestAttributes = typicalBundle.accountRequests.get("instructor1OfCourse1"); - - req = buildCreateRequest(accountRequestAttributes.getName(), - accountRequestAttributes.getInstitute(), accountRequestAttributes.getEmail()); - - InvalidOperationException ioe = verifyInvalidOperation(req); - assertEquals("Cannot create account request as instructor has already registered.", ioe.getMessage()); - ______TS("Error: invalid parameter"); String invalidName = "James%20Bond99"; diff --git a/src/test/java/teammates/ui/webapi/CreateFeedbackSessionLogActionTest.java b/src/test/java/teammates/ui/webapi/CreateFeedbackSessionLogActionTest.java index dd5974c42435..dad8b7ad1a4a 100644 --- a/src/test/java/teammates/ui/webapi/CreateFeedbackSessionLogActionTest.java +++ b/src/test/java/teammates/ui/webapi/CreateFeedbackSessionLogActionTest.java @@ -7,6 +7,7 @@ import teammates.common.datatransfer.attributes.StudentAttributes; import teammates.common.datatransfer.logs.FeedbackSessionLogType; import teammates.common.util.Const; +import teammates.ui.output.MessageOutput; /** * SUT: {@link CreateFeedbackSessionLogAction}. @@ -61,7 +62,9 @@ protected void testExecute() { Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.ACCESS.getLabel(), Const.ParamsNames.STUDENT_EMAIL, student1.getEmail(), }; - getJsonResult(getAction(paramsSuccessfulAccess)); + JsonResult response = getJsonResult(getAction(paramsSuccessfulAccess)); + MessageOutput output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); ______TS("Success case: typical submission"); String[] paramsSuccessfulSubmission = { @@ -70,24 +73,20 @@ protected void testExecute() { Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), Const.ParamsNames.STUDENT_EMAIL, student2.getEmail(), }; - getJsonResult(getAction(paramsSuccessfulSubmission)); + response = getJsonResult(getAction(paramsSuccessfulSubmission)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); ______TS("Success case: should create even for invalid parameters"); - String[] paramsNonExistentCourseId = { - Const.ParamsNames.COURSE_ID, "non-existent-course-id", - Const.ParamsNames.FEEDBACK_SESSION_NAME, fsa1.getFeedbackSessionName(), - Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), - Const.ParamsNames.STUDENT_EMAIL, student1.getEmail(), - }; - getJsonResult(getAction(paramsNonExistentCourseId)); - String[] paramsNonExistentFsName = { Const.ParamsNames.COURSE_ID, courseId1, Const.ParamsNames.FEEDBACK_SESSION_NAME, "non-existent-feedback-session-name", Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), Const.ParamsNames.STUDENT_EMAIL, student1.getEmail(), }; - getJsonResult(getAction(paramsNonExistentFsName)); + response = getJsonResult(getAction(paramsNonExistentFsName)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); String[] paramsNonExistentStudentEmail = { Const.ParamsNames.COURSE_ID, courseId1, @@ -95,7 +94,9 @@ protected void testExecute() { Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), Const.ParamsNames.STUDENT_EMAIL, "non-existent-student@email.com", }; - getJsonResult(getAction(paramsNonExistentStudentEmail)); + response = getJsonResult(getAction(paramsNonExistentStudentEmail)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); ______TS("Success case: should create even when student cannot access feedback session in course"); String[] paramsWithoutAccess = { @@ -104,7 +105,9 @@ protected void testExecute() { Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), Const.ParamsNames.STUDENT_EMAIL, student3.getEmail(), }; - getJsonResult(getAction(paramsWithoutAccess)); + response = getJsonResult(getAction(paramsWithoutAccess)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); } @Test diff --git a/src/test/java/teammates/ui/webapi/GetActionClassesActionTest.java b/src/test/java/teammates/ui/webapi/GetActionClassesActionTest.java index 2c989868edc1..4493e828de61 100644 --- a/src/test/java/teammates/ui/webapi/GetActionClassesActionTest.java +++ b/src/test/java/teammates/ui/webapi/GetActionClassesActionTest.java @@ -86,6 +86,9 @@ protected void testExecute() { CreateAccountRequestAction.class, GetAccountRequestAction.class, DeleteAccountRequestAction.class, + GetAccountRequestsAction.class, + UpdateAccountRequestAction.class, + RejectAccountRequestAction.class, GetAccountAction.class, GetAccountsAction.class, FeedbackSessionPublishedRemindersAction.class, @@ -140,7 +143,8 @@ protected void testExecute() { GetDeadlineExtensionAction.class, SendLoginEmailAction.class, PutSqlDataBundleAction.class, - DeleteSqlDataBundleAction.class + DeleteSqlDataBundleAction.class, + UpdateFeedbackSessionLogsAction.class ); List expectedActionClassesNames = expectedActionClasses.stream() .map(Class::getSimpleName) diff --git a/src/test/java/teammates/ui/webapi/GetAuthInfoActionTest.java b/src/test/java/teammates/ui/webapi/GetAuthInfoActionTest.java index f9d97e6843f0..e57237aaa086 100644 --- a/src/test/java/teammates/ui/webapi/GetAuthInfoActionTest.java +++ b/src/test/java/teammates/ui/webapi/GetAuthInfoActionTest.java @@ -3,7 +3,7 @@ import java.util.ArrayList; import java.util.Arrays; -import javax.servlet.http.Cookie; +import jakarta.servlet.http.Cookie; import org.testng.annotations.Test; diff --git a/src/test/java/teammates/ui/webapi/GetFeedbackSessionLogsActionTest.java b/src/test/java/teammates/ui/webapi/GetFeedbackSessionLogsActionTest.java index c668a9394f81..41b1b0421bc6 100644 --- a/src/test/java/teammates/ui/webapi/GetFeedbackSessionLogsActionTest.java +++ b/src/test/java/teammates/ui/webapi/GetFeedbackSessionLogsActionTest.java @@ -48,15 +48,15 @@ protected void testExecute() { long startTime = endTime - (Const.LOGS_RETENTION_PERIOD.toDays() - 1) * 24 * 60 * 60 * 1000; long invalidStartTime = endTime - (Const.LOGS_RETENTION_PERIOD.toDays() + 1) * 24 * 60 * 60 * 1000; - mockLogsProcessor.insertFeedbackSessionLog(student1Email, fsa1Name, + mockLogsProcessor.insertFeedbackSessionLog(courseId, student1Email, fsa1Name, FeedbackSessionLogType.ACCESS.getLabel(), startTime); - mockLogsProcessor.insertFeedbackSessionLog(student1Email, fsa2Name, + mockLogsProcessor.insertFeedbackSessionLog(courseId, student1Email, fsa2Name, FeedbackSessionLogType.ACCESS.getLabel(), startTime + 1000); - mockLogsProcessor.insertFeedbackSessionLog(student1Email, fsa2Name, + mockLogsProcessor.insertFeedbackSessionLog(courseId, student1Email, fsa2Name, FeedbackSessionLogType.SUBMISSION.getLabel(), startTime + 2000); - mockLogsProcessor.insertFeedbackSessionLog(student2Email, fsa1Name, + mockLogsProcessor.insertFeedbackSessionLog(courseId, student2Email, fsa1Name, FeedbackSessionLogType.ACCESS.getLabel(), startTime + 3000); - mockLogsProcessor.insertFeedbackSessionLog(student2Email, fsa1Name, + mockLogsProcessor.insertFeedbackSessionLog(courseId, student2Email, fsa1Name, FeedbackSessionLogType.SUBMISSION.getLabel(), startTime + 4000); ______TS("Failure case: not enough parameters"); diff --git a/src/test/java/teammates/ui/webapi/JsonResultTest.java b/src/test/java/teammates/ui/webapi/JsonResultTest.java index c691cc75714a..458f6958b5d3 100644 --- a/src/test/java/teammates/ui/webapi/JsonResultTest.java +++ b/src/test/java/teammates/ui/webapi/JsonResultTest.java @@ -3,7 +3,7 @@ import java.util.ArrayList; import java.util.List; -import javax.servlet.http.Cookie; +import jakarta.servlet.http.Cookie; import org.testng.annotations.Test; diff --git a/src/test/resources/emails/adminNewAccountRequestAlertEmailWithComments.html b/src/test/resources/emails/adminNewAccountRequestAlertEmailWithComments.html new file mode 100644 index 000000000000..301fd024a3e7 --- /dev/null +++ b/src/test/resources/emails/adminNewAccountRequestAlertEmailWithComments.html @@ -0,0 +1,60 @@ +

    Hello, Admin

    + +

    + A new instructor account request has been submitted: +

    + +
    + + + + + + + + + + + + + + + + + + + + +
    + + Full Name + + + Anakin Skywalker +
    + + Institute + + + Jedi Order +
    + + Email Address + + + chosen-one@jedi.org +
    + + Comments + + + I don't like sand. It's coarse and rough and irritating... and it gets everywhere. +
    +
    + +Accept/reject this request on the admin panel: ${app.url}/web/admin/home + +

    + Regards,
    + TEAMMATES Team. +

    diff --git a/src/test/resources/emails/adminNewAccountRequestAlertEmailWithNoComments.html b/src/test/resources/emails/adminNewAccountRequestAlertEmailWithNoComments.html new file mode 100644 index 000000000000..a2f62ae17b1d --- /dev/null +++ b/src/test/resources/emails/adminNewAccountRequestAlertEmailWithNoComments.html @@ -0,0 +1,60 @@ +

    Hello, Admin

    + +

    + A new instructor account request has been submitted: +

    + +
    + + + + + + + + + + + + + + + + + + + + +
    + + Full Name + + + Maul +
    + + Institute + + + Sith Order +
    + + Email Address + + + maul@sith.org +
    + + Comments + + + +
    +
    + +Accept/reject this request on the admin panel: ${app.url}/web/admin/home + +

    + Regards,
    + TEAMMATES Team. +

    diff --git a/src/test/resources/emails/instructorAccountRequestRejectionEmail.html b/src/test/resources/emails/instructorAccountRequestRejectionEmail.html new file mode 100644 index 000000000000..57ae404c7e48 --- /dev/null +++ b/src/test/resources/emails/instructorAccountRequestRejectionEmail.html @@ -0,0 +1,10 @@ +

    Hi, Maul

    +

    Thanks for your interest in using TEAMMATES. We are unable to create a TEAMMATES instructor account for you.

    + +

    + Reason: The email address you provided is not an 'official' email address provided by your institution.
    + Remedy: Please re-submit an account request with your 'official' institution email address. +

    + +

    If you need further clarification or would like to appeal this decision, please feel free to contact us at teammates@comp.nus.edu.sg.

    +

    Regards,
    TEAMMATES Team.

    diff --git a/src/test/resources/emails/instructorNewAccountRequestAcknowledgementEmailWithComments.html b/src/test/resources/emails/instructorNewAccountRequestAcknowledgementEmailWithComments.html new file mode 100644 index 000000000000..3d634a788648 --- /dev/null +++ b/src/test/resources/emails/instructorNewAccountRequestAcknowledgementEmailWithComments.html @@ -0,0 +1,65 @@ +

    Hello, Darth Vader

    + +

    + Thank you for submitting an account request. This is what you have submitted: +

    + +
    + + + + + + + + + + + + + + + + + + + + +
    + + Full Name + + + Darth Vader +
    + + Country & Institute + + + Sith Order +
    + + Email Address + + + darth-vader@sith.org +
    + + Comments + + + I Am Your Father +
    +
    + +

    + Your request will be reviewed within 24 hours. We will send another email once your request has been accepted. +

    +

    + If you have any additional queries, please feel free to contact us at ${support.email}. +

    + +

    + Regards,
    + TEAMMATES Team. +

    diff --git a/src/test/resources/emails/instructorNewAccountRequestAcknowledgementEmailWithNoComments.html b/src/test/resources/emails/instructorNewAccountRequestAcknowledgementEmailWithNoComments.html new file mode 100644 index 000000000000..9460b051df5a --- /dev/null +++ b/src/test/resources/emails/instructorNewAccountRequestAcknowledgementEmailWithNoComments.html @@ -0,0 +1,65 @@ +

    Hello, Maul

    + +

    + Thank you for submitting an account request. This is what you have submitted: +

    + +
    + + + + + + + + + + + + + + + + + + + + +
    + + Full Name + + + Maul +
    + + Country & Institute + + + Sith Order +
    + + Email Address + + + maul@sith.org +
    + + Comments + + + +
    +
    + +

    + Your request will be reviewed within 24 hours. We will send another email once your request has been accepted. +

    +

    + If you have any additional queries, please feel free to contact us at ${support.email}. +

    + +

    + Regards,
    + TEAMMATES Team. +

    diff --git a/src/web/app/components/account-requests-table/__snapshots__/account-request-table.component.spec.ts.snap b/src/web/app/components/account-requests-table/__snapshots__/account-request-table.component.spec.ts.snap new file mode 100644 index 000000000000..bbf59a6a945d --- /dev/null +++ b/src/web/app/components/account-requests-table/__snapshots__/account-request-table.component.spec.ts.snap @@ -0,0 +1,750 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountRequestTableComponent should display account requests with no reset or expand links button 1`] = ` + +
    +
    + + Pending Account Requests + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Name + + Email + + Status + + Institute, Country + + Created At + + Comments + + Options +
    + name + + email + + PENDING + + institute + + Tue, 08 Feb 2022, 08:23 AM +00:00 + +
    + comment +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    + + + + + +
    +
    + name + + email + + PENDING + + institute + + Tue, 08 Feb 2022, 08:23 AM +00:00 + +
    + comment +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    + + + + + +
    +
    +
    +
    +
    +`; + +exports[`AccountRequestTableComponent should display account requests with reset button and expandable links buttons 1`] = ` + +
    +
    +
    + + Account Requests Found + +
    +
    + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Name + + Email + + Status + + Institute, Country + + Created At + + Registered At + + Comments + + Options +
    + name + + email + + APPROVED + + institute + + Tue, 08 Feb 2022, 08:23 AM +00:00 + + Not Registered Yet + +
    + comment +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    + + + + + +
    + +
    +
    +
    + name + + email + + REGISTERED + + institute + + Tue, 08 Feb 2022, 08:23 AM +00:00 + + Not Registered Yet + +
    + comment +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    + + + + + +
    + +
    +
    +
    +
    +
    +
    +`; + +exports[`AccountRequestTableComponent should snap with an expanded account requests table 1`] = ` + +
    +
    + + Pending Account Requests + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + Name + + Email + + Status + + Institute, Country + + Created At + + Comments + + Options +
    + name + + email + + PENDING + + institute + + Tue, 08 Feb 2022, 08:23 AM +00:00 + +
    + comment +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    + + + + + +
    +
    +
    +
    +
    +`; diff --git a/src/web/app/components/account-requests-table/account-request-table-model.ts b/src/web/app/components/account-requests-table/account-request-table-model.ts new file mode 100644 index 000000000000..1dc11a3b43c1 --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table-model.ts @@ -0,0 +1,17 @@ +import { AccountRequestStatus } from 'src/web/types/api-output'; + +/** + * Model for the row entries in the account requests table. + */ +export interface AccountRequestTableRowModel { + id: string; + name: string; + email: string; + status: AccountRequestStatus; + instituteAndCountry: string; + createdAtText: string; + registeredAtText: string; + comments: string; + registrationLink: string; + showLinks: boolean; +} diff --git a/src/web/app/components/account-requests-table/account-request-table.component.html b/src/web/app/components/account-requests-table/account-request-table.component.html new file mode 100644 index 000000000000..924cb5592d09 --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table.component.html @@ -0,0 +1,96 @@ +
    +
    +
    + Account Requests Found +
    + + Pending Account Requests + +
    + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameEmailStatusInstitute, CountryCreated AtRegistered AtCommentsOptions
    +
    +
    + + +
    +
    {{ accountRequest.email }}{{ accountRequest.status }}{{ accountRequest.instituteAndCountry }}{{ accountRequest.createdAtText }}{{ accountRequest.registeredAtText || 'Not Registered Yet' }} +
    + {{ accountRequest.comments }} +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    + + + +
    + + +
    +
    +
    + +
    +
    +
    +
      +
    • + Account Registration Link + +
    • +
    +
    +
    diff --git a/src/web/app/components/account-requests-table/account-request-table.component.scss b/src/web/app/components/account-requests-table/account-request-table.component.scss new file mode 100644 index 000000000000..6af57f073090 --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table.component.scss @@ -0,0 +1,65 @@ +::ng-deep .highlighted-text { + background-color: yellow; +} + +/* stylelint-disable declaration-block-no-redundant-longhand-properties */ +.table-responsive { + overflow-y: visible; + overflow-x: -moz-scrollbars-horizontal; +} + +.table-responsive > table > thead > tr > th { + white-space: nowrap; +} + +/* stylelint-disable property-no-vendor-prefix */ +::-webkit-scrollbar { + -webkit-appearance: none; + width: 1px; +} + +::-webkit-scrollbar-thumb { + border-radius: 0; + background-color: rgb(0 0 0 / 50%); + box-shadow: 0 0 1px rgb(255 255 255 / 50%); +} + + +#search-table-account-request { + border-collapse: collapse; +} + + +#search-table-account-request th:last-child, +#search-table-account-request td:last-child { + min-width: 10vw; + position: sticky; + right: 0; + z-index: 1; + background-color: #F8F9FA; +} + +#search-table-account-request th:last-child::after, +#search-table-account-request td:last-child::after { + content: ""; + position: absolute; + left: -1px; + top: 0; + bottom: 0; + width: 1px; + background: #c8c7c7; + z-index: 1; +} + +#comment-box { + min-height: 5vh; + width: max(800px, 35vw); + max-width: max-content; + word-break: break-word; + word-wrap: break-all; + +} + +.dropdown-item { + border: none; +} diff --git a/src/web/app/components/account-requests-table/account-request-table.component.spec.ts b/src/web/app/components/account-requests-table/account-request-table.component.spec.ts new file mode 100644 index 000000000000..40ccbc007c3f --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table.component.spec.ts @@ -0,0 +1,509 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { of, throwError } from 'rxjs'; +import { AccountRequestTableRowModel } from './account-request-table-model'; +import { AccountRequestTableComponent } from './account-request-table.component'; +import { AccountRequestTableModule } from './account-request-table.module'; +import { EditRequestModalComponent } from './admin-edit-request-modal/admin-edit-request-modal.component'; +import { + RejectWithReasonModalComponent, +} from './admin-reject-with-reason-modal/admin-reject-with-reason-modal.component'; +import { AccountService } from '../../../services/account.service'; +import { SimpleModalService } from '../../../services/simple-modal.service'; +import { StatusMessageService } from '../../../services/status-message.service'; +import { createBuilder } from '../../../test-helpers/generic-builder'; +import { createMockNgbModalRef } from '../../../test-helpers/mock-ngb-modal-ref'; +import { AccountRequest, AccountRequestStatus } from '../../../types/api-output'; +import { SimpleModalType } from '../simple-modal/simple-modal-type'; + +describe('AccountRequestTableComponent', () => { + let component: AccountRequestTableComponent; + let fixture: ComponentFixture; + let accountService: AccountService; + let statusMessageService: StatusMessageService; + let simpleModalService: SimpleModalService; + let ngbModal: NgbModal; + + const accountRequestDetailsBuilder = createBuilder({ + id: '', + email: '', + name: '', + instituteAndCountry: '', + registrationLink: '', + status: AccountRequestStatus.PENDING, + comments: '', + registeredAtText: '', + createdAtText: '', + showLinks: false, + }); + + const DEFAULT_ACCOUNT_REQUEST = accountRequestDetailsBuilder + .email('email') + .name('name') + .status(AccountRequestStatus.PENDING) + .instituteAndCountry('institute') + .createdAtText('Tue, 08 Feb 2022, 08:23 AM +00:00') + .comments('comment'); + + const resetModalContent = `Are you sure you want to reset the account request for + name with email email from + institute? + An email with the account registration link will also be sent to the instructor.`; + const resetModalTitle = 'Reset account request for name?'; + const deleteModalContent = `Are you sure you want to delete the account request for + name with email email from + institute?`; + const deleteModalTitle = 'Delete account request for name?'; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [AccountRequestTableComponent], + imports: [ + AccountRequestTableModule, + BrowserAnimationsModule, + HttpClientTestingModule, + ], + providers: [ + AccountService, SimpleModalService, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AccountRequestTableComponent); + component = fixture.componentInstance; + accountService = TestBed.inject(AccountService); + statusMessageService = TestBed.inject(StatusMessageService); + simpleModalService = TestBed.inject(SimpleModalService); + ngbModal = TestBed.inject(NgbModal); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should snap with an expanded account requests table', () => { + const accountRequestResult: AccountRequestTableRowModel = DEFAULT_ACCOUNT_REQUEST.build(); + component.accountRequests = [ + accountRequestResult, + ]; + + fixture.detectChanges(); + expect(fixture).toMatchSnapshot(); + }); + + it('should show account request links when expand all button clicked', () => { + const accountRequestResult: AccountRequestTableRowModel = DEFAULT_ACCOUNT_REQUEST.build(); + accountRequestResult.status = AccountRequestStatus.APPROVED; + accountRequestResult.registrationLink = 'registrationLink'; + component.accountRequests = [ + accountRequestResult, + ]; + component.searchString = 'test'; + fixture.detectChanges(); + + const button: any = fixture.debugElement.nativeElement.querySelector('#show-account-request-links'); + button.click(); + expect(component.accountRequests[0].showLinks).toEqual(true); + }); + + it('should display account requests with no reset or expand links button', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + expect(fixture).toMatchSnapshot(); + }); + + it('should display account requests with reset button and expandable links buttons', + () => { + const approvedAccountRequestResult: AccountRequestTableRowModel = DEFAULT_ACCOUNT_REQUEST.build(); + approvedAccountRequestResult.status = AccountRequestStatus.APPROVED; + approvedAccountRequestResult.registrationLink = 'registrationLink'; + + const registeredAccountRequestResult: AccountRequestTableRowModel = DEFAULT_ACCOUNT_REQUEST.build(); + registeredAccountRequestResult.status = AccountRequestStatus.REGISTERED; + registeredAccountRequestResult.registrationLink = 'registrationLink'; + + const accountRequestResults: AccountRequestTableRowModel[] = [ + approvedAccountRequestResult, + registeredAccountRequestResult, + ]; + + component.accountRequests = accountRequestResults; + component.searchString = 'test'; + fixture.detectChanges(); + expect(fixture).toMatchSnapshot(); + }); + + it('should show success message when deleting account request is successful', () => { + component.accountRequests = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + fixture.detectChanges(); + + const modalSpy = jest.spyOn(simpleModalService, 'openConfirmationModal').mockImplementation(() => { + return createMockNgbModalRef({}); + }); + + jest.spyOn(accountService, 'deleteAccountRequest').mockReturnValue(of({ + message: 'Account request successfully deleted.', + })); + + const spyStatusMessageService: any = jest.spyOn(statusMessageService, 'showSuccessToast') + .mockImplementation((args: string) => { + expect(args).toEqual('Account request successfully deleted.'); + }); + + const deleteButton: any = fixture.debugElement.nativeElement.querySelector('#delete-account-request-0'); + deleteButton.click(); + + expect(spyStatusMessageService).toHaveBeenCalled(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith(deleteModalTitle, SimpleModalType.DANGER, deleteModalContent); + }); + + it('should show error message when deleting account request is unsuccessful', () => { + component.accountRequests = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + fixture.detectChanges(); + + const modalSpy = jest.spyOn(simpleModalService, 'openConfirmationModal').mockImplementation(() => { + return createMockNgbModalRef({}); + }); + + jest.spyOn(accountService, 'deleteAccountRequest').mockReturnValue(throwError(() => ({ + error: { + message: 'This is the error message.', + }, + }))); + + const spyStatusMessageService: any = jest.spyOn(statusMessageService, 'showErrorToast') + .mockImplementation((args: string) => { + expect(args).toEqual('This is the error message.'); + }); + + const deleteButton: any = fixture.debugElement.nativeElement.querySelector('#delete-account-request-0'); + deleteButton.click(); + + expect(spyStatusMessageService).toHaveBeenCalled(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith(deleteModalTitle, SimpleModalType.DANGER, deleteModalContent); + }); + + it('should show success message when resetting account request is successful', () => { + const registeredAccountRequestResult: AccountRequestTableRowModel = DEFAULT_ACCOUNT_REQUEST.build(); + registeredAccountRequestResult.status = AccountRequestStatus.REGISTERED; + registeredAccountRequestResult.registrationLink = 'registrationLink'; + registeredAccountRequestResult.registeredAtText = 'registeredTime'; + component.accountRequests = [ + registeredAccountRequestResult, + ]; + + component.searchString = 'test'; + fixture.detectChanges(); + + const modalSpy = jest.spyOn(simpleModalService, 'openConfirmationModal').mockImplementation(() => { + return createMockNgbModalRef({}); + }); + + jest.spyOn(accountService, 'resetAccountRequest').mockReturnValue(of({ + joinLink: 'joinlink', + })); + + const spyStatusMessageService = jest.spyOn(statusMessageService, 'showSuccessToast') + .mockImplementation((args: string) => { + expect(args) + .toEqual('Reset successful. An email has been sent to email.'); + }); + + const resetButton = fixture.debugElement.nativeElement.querySelector('#reset-account-request-0'); + resetButton.click(); + + expect(spyStatusMessageService).toHaveBeenCalled(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith(resetModalTitle, SimpleModalType.WARNING, resetModalContent); + }); + + it('should show error message when resetting account request is unsuccessful', () => { + const registeredAccountRequestResult: AccountRequestTableRowModel = DEFAULT_ACCOUNT_REQUEST.build(); + registeredAccountRequestResult.status = AccountRequestStatus.REGISTERED; + registeredAccountRequestResult.registrationLink = 'registrationLink'; + registeredAccountRequestResult.registeredAtText = 'registeredTime'; + component.accountRequests = [ + registeredAccountRequestResult, + ]; + + component.searchString = 'test'; + fixture.detectChanges(); + + const modalSpy = jest.spyOn(simpleModalService, 'openConfirmationModal').mockImplementation(() => { + return createMockNgbModalRef({}); + }); + + jest.spyOn(accountService, 'resetAccountRequest').mockReturnValue(throwError(() => ({ + error: { + message: 'This is the error message.', + }, + }))); + + const spyStatusMessageService = jest.spyOn(statusMessageService, 'showErrorToast') + .mockImplementation((args: string) => { + expect(args).toEqual('This is the error message.'); + }); + + const resetButton = fixture.debugElement.nativeElement.querySelector('#reset-account-request-0'); + resetButton.click(); + + expect(spyStatusMessageService).toHaveBeenCalled(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith(resetModalTitle, SimpleModalType.WARNING, resetModalContent); + }); + + it('should display comment modal', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + const modalSpy = jest.spyOn(simpleModalService, 'openInformationModal') + .mockReturnValue(createMockNgbModalRef()); + + const viewCommentButton: any = fixture.debugElement.nativeElement.querySelector('#view-account-request-0'); + viewCommentButton.click(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith('Comments for name Request', + SimpleModalType.INFO, 'Comment: comment'); + }); + + it('should display edit modal when edit button is clicked', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + const mockModalRef = { + componentInstance: {}, + result: Promise.resolve({}), + }; + + const modalSpy = jest.spyOn(ngbModal, 'open').mockReturnValue(mockModalRef as any); + + const editButton: any = fixture.debugElement.nativeElement.querySelector('#edit-account-request-0'); + editButton.click(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith(EditRequestModalComponent); + }); + + it('should display reject modal when reject button is clicked', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + const mockModalRef = { + componentInstance: {}, + result: Promise.resolve({}), + }; + + const modalSpy = jest.spyOn(ngbModal, 'open').mockReturnValue(mockModalRef as any); + + const rejectButton: any = fixture.debugElement.nativeElement.querySelector('#reject-request-with-reason-0'); + rejectButton.click(); + fixture.detectChanges(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith(RejectWithReasonModalComponent); + }); + + it('should display error message when rejection was unsuccessful', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + jest.spyOn(accountService, 'rejectAccountRequest').mockReturnValue(throwError(() => ({ + error: { + message: 'This is the error message.', + }, + }))); + + const spyStatusMessageService = jest.spyOn(statusMessageService, 'showErrorToast') + .mockImplementation((args: string) => { + expect(args).toEqual('This is the error message.'); + }); + + const rejectButton = fixture.debugElement.nativeElement.querySelector('#reject-request-0'); + rejectButton.click(); + + expect(spyStatusMessageService).toHaveBeenCalled(); + }); + + it('should display error message when approval was unsuccessful', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + jest.spyOn(accountService, 'approveAccountRequest').mockReturnValue(throwError(() => ({ + error: { + message: 'This is the error message.', + }, + }))); + + const spyStatusMessageService: any = jest.spyOn(statusMessageService, 'showErrorToast') + .mockImplementation((args: string) => { + expect(args).toEqual('This is the error message.'); + }); + + const approveButton: any = fixture.debugElement.nativeElement.querySelector('#approve-account-request-0'); + approveButton.click(); + + expect(spyStatusMessageService).toHaveBeenCalled(); + }); + + it('should display error message when edit was unsuccessful', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + const mockModalRef = { + componentInstance: {}, + result: Promise.resolve({}), + }; + + jest.spyOn(ngbModal, 'open').mockReturnValue(mockModalRef as any); + + jest.spyOn(accountService, 'editAccountRequest').mockReturnValue(throwError(() => ({ + error: { + message: 'This is the error message.', + }, + }))); + + const spyStatusMessageService = jest.spyOn(statusMessageService, 'showErrorToast') + .mockImplementation((args: string) => { + expect(args).toEqual('This is the error message.'); + }); + + const editButton = fixture.debugElement.nativeElement.querySelector('#edit-account-request-0'); + editButton.click(); + + expect(spyStatusMessageService).toHaveBeenCalled(); + }); + + it('should update request when edit is succcessful', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + const mockModalRef = { + componentInstance: {}, + result: Promise.resolve({}), + }; + + const modalSpy = jest.spyOn(ngbModal, 'open').mockReturnValue(mockModalRef as any); + + const editedAccountRequest : AccountRequest = { + id: 'id', + comments: 'new comment', + email: 'new email', + institute: 'new institute', + registrationKey: 'registration key', + name: 'new name', + createdAt: 1, + status: AccountRequestStatus.PENDING, + }; + + jest.spyOn(accountService, 'editAccountRequest').mockReturnValue(of(editedAccountRequest)); + + const editButton: any = fixture.debugElement.nativeElement.querySelector('#edit-account-request-0'); + editButton.click(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith(EditRequestModalComponent); + + fixture.detectChanges(); + expect(component.accountRequests[0].comments).toEqual('new comment'); + expect(component.accountRequests[0].email).toEqual('new email'); + expect(component.accountRequests[0].instituteAndCountry).toEqual('new institute'); + expect(component.accountRequests[0].name).toEqual('new name'); + }); + + it('should update status when approval is succcessful', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + const approvedRequest : AccountRequest = { + id: component.accountRequests[0].id, + comments: component.accountRequests[0].comments, + email: component.accountRequests[0].email, + institute: component.accountRequests[0].instituteAndCountry, + registrationKey: 'registration key', + name: component.accountRequests[0].name, + createdAt: 1, + status: AccountRequestStatus.APPROVED, + }; + + jest.spyOn(accountService, 'approveAccountRequest').mockReturnValue(of(approvedRequest)); + + const approveButton: any = fixture.debugElement.nativeElement.querySelector('#approve-account-request-0'); + approveButton.click(); + + fixture.detectChanges(); + expect(component.accountRequests[0].status).toEqual(AccountRequestStatus.APPROVED); + }); + + it('should update status when rejection is succcessful', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + const rejectedRequest : AccountRequest = { + id: component.accountRequests[0].id, + comments: component.accountRequests[0].comments, + email: component.accountRequests[0].email, + institute: component.accountRequests[0].instituteAndCountry, + registrationKey: 'registration key', + name: component.accountRequests[0].name, + createdAt: 1, + status: AccountRequestStatus.REJECTED, + }; + + jest.spyOn(accountService, 'rejectAccountRequest').mockReturnValue(of(rejectedRequest)); + + const rejectButton: any = fixture.debugElement.nativeElement.querySelector('#reject-request-0'); + rejectButton.click(); + + fixture.detectChanges(); + expect(component.accountRequests[0].status).toEqual(AccountRequestStatus.REJECTED); + }); +}); diff --git a/src/web/app/components/account-requests-table/account-request-table.component.ts b/src/web/app/components/account-requests-table/account-request-table.component.ts new file mode 100755 index 000000000000..07ba413d77bb --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table.component.ts @@ -0,0 +1,199 @@ +import { Component, Input } from '@angular/core'; +import { NgbModalRef, NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { AccountRequestTableRowModel } from './account-request-table-model'; +import { EditRequestModalComponentResult } from './admin-edit-request-modal/admin-edit-request-modal-model'; +import { EditRequestModalComponent } from './admin-edit-request-modal/admin-edit-request-modal.component'; +import { + RejectWithReasonModalComponentResult, +} from './admin-reject-with-reason-modal/admin-reject-with-reason-modal-model'; +import { + RejectWithReasonModalComponent, +} from './admin-reject-with-reason-modal/admin-reject-with-reason-modal.component'; +import { AccountService } from '../../../services/account.service'; +import { SimpleModalService } from '../../../services/simple-modal.service'; +import { StatusMessageService } from '../../../services/status-message.service'; +import { AccountRequest, MessageOutput } from '../../../types/api-output'; +import { ErrorMessageOutput } from '../../error-message-output'; +import { SimpleModalType } from '../simple-modal/simple-modal-type'; +import { collapseAnim } from '../teammates-common/collapse-anim'; + +/** + * Account requests table component. + */ +@Component({ + selector: 'tm-account-request-table', + templateUrl: './account-request-table.component.html', + styleUrls: ['./account-request-table.component.scss'], + animations: [collapseAnim], +}) + +export class AccountRequestTableComponent { + + @Input() + accountRequests: AccountRequestTableRowModel[] = []; + + @Input() + searchString = ''; + + constructor( + private statusMessageService: StatusMessageService, + private simpleModalService: SimpleModalService, + private accountService: AccountService, + private ngbModal: NgbModal, + ) {} + + /** + * Shows all account requests' links in the page. + */ + showAllAccountRequestsLinks(): void { + for (const accountRequest of this.accountRequests) { + accountRequest.showLinks = true; + } + } + + /** + * Hides all account requests' links in the page. + */ + hideAllAccountRequestsLinks(): void { + for (const accountRequest of this.accountRequests) { + accountRequest.showLinks = false; + } + } + + editAccountRequest(accountRequest: AccountRequestTableRowModel): void { + const modalRef: NgbModalRef = this.ngbModal.open(EditRequestModalComponent); + modalRef.componentInstance.accountRequestName = accountRequest.name; + modalRef.componentInstance.accountRequestEmail = accountRequest.email; + modalRef.componentInstance.accountRequestInstitution = accountRequest.instituteAndCountry; + modalRef.componentInstance.accountRequestComments = accountRequest.comments; + + modalRef.result.then((res: EditRequestModalComponentResult) => { + this.accountService.editAccountRequest( + accountRequest.id, + res.accountRequestName, + res.accountRequestEmail, + res.accountRequestInstitution, + accountRequest.status, + res.accountRequestComment) + .subscribe({ + next: (resp: AccountRequest) => { + accountRequest.comments = resp.comments ?? ''; + accountRequest.name = resp.name; + accountRequest.email = resp.email; + accountRequest.instituteAndCountry = resp.institute; + this.statusMessageService.showSuccessToast('Account request was successfully updated.'); + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + }, () => {}); + } + + approveAccountRequest(accountRequest: AccountRequestTableRowModel): void { + this.accountService.approveAccountRequest(accountRequest.id, accountRequest.name, + accountRequest.email, accountRequest.instituteAndCountry) + .subscribe({ + next: (resp : AccountRequest) => { + accountRequest.status = resp.status; + this.statusMessageService.showSuccessToast( + `Account request was successfully approved. Email has been sent to ${accountRequest.email}.`, + ); + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + } + + resetAccountRequest(accountRequest: AccountRequestTableRowModel): void { + const modalContent = `Are you sure you want to reset the account request for + ${accountRequest.name} with email ${accountRequest.email} from + ${accountRequest.instituteAndCountry}? + An email with the account registration link will also be sent to the instructor.`; + const modalRef: NgbModalRef = this.simpleModalService.openConfirmationModal( + `Reset account request for ${accountRequest.name}?`, SimpleModalType.WARNING, modalContent); + + modalRef.result.then(() => { + this.accountService.resetAccountRequest(accountRequest.id) + .subscribe({ + next: () => { + this.statusMessageService + .showSuccessToast(`Reset successful. An email has been sent to ${accountRequest.email}.`); + accountRequest.registeredAtText = ''; + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + }, () => {}); + } + + deleteAccountRequest(accountRequest: AccountRequestTableRowModel): void { + const modalContent: string = `Are you sure you want to delete the account request for + ${accountRequest.name} with email ${accountRequest.email} from + ${accountRequest.instituteAndCountry}?`; + const modalRef: NgbModalRef = this.simpleModalService.openConfirmationModal( + `Delete account request for ${accountRequest.name}?`, SimpleModalType.DANGER, modalContent); + + modalRef.result.then(() => { + this.accountService.deleteAccountRequest(accountRequest.id) + .subscribe({ + next: (resp: MessageOutput) => { + this.statusMessageService.showSuccessToast(resp.message); + this.accountRequests = this.accountRequests.filter((x: AccountRequestTableRowModel) => x !== accountRequest); + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + }, () => {}); + } + + viewAccountRequest(accountRequest: AccountRequestTableRowModel): void { + const modalContent: string = `Comment: ${accountRequest.comments || 'No comments'}`; + const modalRef: NgbModalRef = this.simpleModalService.openInformationModal( + `Comments for ${accountRequest.name} Request`, SimpleModalType.INFO, modalContent); + + modalRef.result.then(() => {}, () => {}); + } + + rejectAccountRequest(accountRequest: AccountRequestTableRowModel): void { + this.accountService.rejectAccountRequest(accountRequest.id) + .subscribe({ + next: (resp : AccountRequest) => { + accountRequest.status = resp.status; + this.statusMessageService.showSuccessToast('Account request was successfully rejected.'); + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + } + + rejectAccountRequestWithReason(accountRequest: AccountRequestTableRowModel): void { + const modalRef: NgbModalRef = this.ngbModal.open(RejectWithReasonModalComponent); + modalRef.componentInstance.accountRequestName = accountRequest.name; + modalRef.componentInstance.accountRequestEmail = accountRequest.email; + + modalRef.result.then((res: RejectWithReasonModalComponentResult) => { + this.accountService.rejectAccountRequest(accountRequest.id, + res.rejectionReasonTitle, res.rejectionReasonBody) + .subscribe({ + next: (resp: AccountRequest) => { + accountRequest.status = resp.status; + this.statusMessageService.showSuccessToast( + `Account request was successfully rejected. Email has been sent to ${accountRequest.email}.`, + ); + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + }, () => {}); + } + + trackAccountRequest(accountRequest: AccountRequestTableRowModel): string { + return accountRequest.id; + } +} diff --git a/src/web/app/components/account-requests-table/account-request-table.module.ts b/src/web/app/components/account-requests-table/account-request-table.module.ts new file mode 100644 index 000000000000..2ff431b10219 --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table.module.ts @@ -0,0 +1,34 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbTooltipModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { AccountRequestTableComponent } from './account-request-table.component'; +import { EditRequestModalComponent } from './admin-edit-request-modal/admin-edit-request-modal.component'; +import { + RejectWithReasonModalComponent, +} from './admin-reject-with-reason-modal/admin-reject-with-reason-modal.component'; +import { Pipes } from '../../pipes/pipes.module'; +import { RichTextEditorModule } from '../rich-text-editor/rich-text-editor.module'; + +/** + * Module for account requests table. + */ +@NgModule({ + declarations: [ + AccountRequestTableComponent, + EditRequestModalComponent, + RejectWithReasonModalComponent, + ], + exports: [ + AccountRequestTableComponent, + ], + imports: [ + CommonModule, + FormsModule, + NgbTooltipModule, + NgbDropdownModule, + Pipes, + RichTextEditorModule, + ], +}) +export class AccountRequestTableModule { } diff --git a/src/web/app/components/account-requests-table/admin-edit-request-modal/__snapshots__/admin-edit-request-modal.component.spec.ts.snap b/src/web/app/components/account-requests-table/admin-edit-request-modal/__snapshots__/admin-edit-request-modal.component.spec.ts.snap new file mode 100644 index 000000000000..17770c36eb2a --- /dev/null +++ b/src/web/app/components/account-requests-table/admin-edit-request-modal/__snapshots__/admin-edit-request-modal.component.spec.ts.snap @@ -0,0 +1,215 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RejectWithReasonModal should show empty fields 1`] = ` + + + +`; diff --git a/src/web/app/pages-static/request-page/instructor-request-form/__snapshots__/instructor-request-form.component.spec.ts.snap b/src/web/app/pages-static/request-page/instructor-request-form/__snapshots__/instructor-request-form.component.spec.ts.snap new file mode 100644 index 000000000000..46dcb75b2cfc --- /dev/null +++ b/src/web/app/pages-static/request-page/instructor-request-form/__snapshots__/instructor-request-form.component.spec.ts.snap @@ -0,0 +1,197 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InstructorRequestFormComponent should render correctly 1`] = ` + +
    +
    + +

    + This is the name that will be shown to your students. You may include salutation (Dr. Prof. etc.) +

    + + +
    +
    +
    + +

    + Please give full name of the university/institution. +

    + + +
    +
    +
    + +

    + Which country is your university/institution based in? +

    + + +
    +
    +
    + +

    + Please use the email address + + given to you by your school/university + + (not your personal Gmail/Hotmail address). Note that this email address will be visible to the students you enroll in TEAMMATES. +

    + + +
    +
    +
    + + +
    +
    + + There was a problem with your submission. Please check and fix the errors above and submit again. + + + Error submitting request: {{serverErrorMessage}} + + +
    diff --git a/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.scss b/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.scss new file mode 100644 index 000000000000..3e5249ca353a --- /dev/null +++ b/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.scss @@ -0,0 +1,26 @@ +label.qn { + font-weight: bold; + font-size: 1rem; + margin-bottom: 0.3rem; +} + +.form-group { + margin-bottom: 0.5rem; +} + +.form-group.required > label::after { + content:"*"; + color: red; +} + +.help-block { + margin-bottom: 0.8rem; +} + +.red-font { + color: red; +} + +.error-box { + margin: 1rem 0; +} diff --git a/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.spec.ts b/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.spec.ts new file mode 100644 index 000000000000..3a5f4fba2b8b --- /dev/null +++ b/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.spec.ts @@ -0,0 +1,111 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { Observable, first } from 'rxjs'; +import { InstructorRequestFormModel } from './instructor-request-form-model'; +import { InstructorRequestFormComponent } from './instructor-request-form.component'; +import { AccountService } from '../../../../services/account.service'; +import { AccountCreateRequest } from '../../../../types/api-request'; + +describe('InstructorRequestFormComponent', () => { + let component: InstructorRequestFormComponent; + let fixture: ComponentFixture; + let accountService: AccountService; + const typicalModel: InstructorRequestFormModel = { + name: 'John Doe', + institution: 'Example Institution', + country: 'Example Country', + email: 'jd@example.edu', + comments: '', + }; + const typicalCreateRequest: AccountCreateRequest = { + instructorEmail: typicalModel.email, + instructorName: typicalModel.name, + instructorInstitution: `${typicalModel.institution}, ${typicalModel.country}`, + }; + + const accountServiceStub: Partial = { + createAccountRequest: () => new Observable((subscriber) => { + subscriber.next(); + }), + }; + + /** + * Fills in form fields with the given data. + * + * @param data Data to fill form with. + */ + function fillFormWith(data: InstructorRequestFormModel): void { + component.name.setValue(data.name); + component.institution.setValue(data.institution); + component.country.setValue(data.country); + component.email.setValue(data.email); + component.comments.setValue(data.comments); + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [InstructorRequestFormComponent], + imports: [ReactiveFormsModule], + providers: [{ provide: AccountService, useValue: accountServiceStub }], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(InstructorRequestFormComponent); + component = fixture.componentInstance; + accountService = TestBed.inject(AccountService); + fixture.detectChanges(); + jest.clearAllMocks(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render correctly', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should run onSubmit() when submit button is clicked', () => { + jest.spyOn(component, 'onSubmit'); + + fillFormWith(typicalModel); + const submitButton = fixture.debugElement.query(By.css('#submit-button')); + submitButton.nativeElement.click(); + + expect(component.onSubmit).toHaveBeenCalledTimes(1); + }); + + it('should emit requestSubmissionEvent with the correct data when form is submitted', () => { + jest.spyOn(accountService, 'createAccountRequest').mockReturnValue( + new Observable((subscriber) => { subscriber.next(); })); + + // Listen for emitted value + let actualModel: InstructorRequestFormModel | null = null; + component.requestSubmissionEvent.pipe(first()) + .subscribe((data: InstructorRequestFormModel) => { actualModel = data; }); + + fillFormWith(typicalModel); + component.onSubmit(); + + expect(actualModel).toBeTruthy(); + expect(actualModel!.name).toBe(typicalModel.name); + expect(actualModel!.institution).toBe(typicalModel.institution); + expect(actualModel!.country).toBe(typicalModel.country); + expect(actualModel!.email).toBe(typicalModel.email); + expect(actualModel!.comments).toBe(typicalModel.comments); + }); + + it('should send the correct request data when form is submitted', () => { + jest.spyOn(accountService, 'createAccountRequest').mockReturnValue( + new Observable((subscriber) => { subscriber.next(); })); + + fillFormWith(typicalModel); + component.onSubmit(); + + expect(accountService.createAccountRequest).toHaveBeenCalledTimes(1); + expect(accountService.createAccountRequest).toHaveBeenCalledWith(expect.objectContaining(typicalCreateRequest)); + }); +}); diff --git a/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.ts b/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.ts new file mode 100644 index 000000000000..02b2ae00fd2d --- /dev/null +++ b/src/web/app/pages-static/request-page/instructor-request-form/instructor-request-form.component.ts @@ -0,0 +1,130 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { finalize } from 'rxjs'; +import { InstructorRequestFormModel } from './instructor-request-form-model'; +import { AccountService } from '../../../../services/account.service'; +import { AccountCreateRequest } from '../../../../types/api-request'; +import { FormValidator } from '../../../../types/form-validator'; +import { ErrorMessageOutput } from '../../../error-message-output'; + +@Component({ + selector: 'tm-instructor-request-form', + templateUrl: './instructor-request-form.component.html', + styleUrls: ['./instructor-request-form.component.scss'], +}) +export class InstructorRequestFormComponent { + + constructor(private accountService: AccountService) {} + + // Create members to be accessed in template + readonly STUDENT_NAME_MAX_LENGTH = FormValidator.STUDENT_NAME_MAX_LENGTH; + readonly INSTITUTION_NAME_MAX_LENGTH = FormValidator.INSTITUTION_NAME_MAX_LENGTH; + readonly COUNTRY_NAME_MAX_LENGTH = FormValidator.COUNTRY_NAME_MAX_LENGTH; + readonly EMAIL_MAX_LENGTH = FormValidator.EMAIL_MAX_LENGTH; + + arf = new FormGroup({ + name: new FormControl('', [ + Validators.required, + Validators.maxLength(FormValidator.STUDENT_NAME_MAX_LENGTH), + Validators.pattern(FormValidator.NAME_REGEX), + ]), + institution: new FormControl('', [ + Validators.required, + Validators.maxLength(FormValidator.INSTITUTION_NAME_MAX_LENGTH), + Validators.pattern(FormValidator.NAME_REGEX), + ]), + country: new FormControl('', [ + Validators.required, + Validators.maxLength(FormValidator.COUNTRY_NAME_MAX_LENGTH), + Validators.pattern(FormValidator.NAME_REGEX), + ]), + email: new FormControl('', [ + Validators.required, + Validators.pattern(FormValidator.EMAIL_REGEX), + Validators.maxLength(FormValidator.EMAIL_MAX_LENGTH), + ]), + comments: new FormControl(''), + }, { updateOn: 'submit' }); + + // Create members for easier access of arf controls + name = this.arf.controls.name; + institution = this.arf.controls.institution; + country = this.arf.controls.country; + email = this.arf.controls.email; + comments = this.arf.controls.comments; + + hasSubmitAttempt = false; + isLoading = false; + @Output() requestSubmissionEvent = new EventEmitter(); + + serverErrorMessage = ''; + + checkIsFieldRequired(field: FormControl): boolean { + return field.hasValidator(Validators.required); + } + + get canSubmit(): boolean { + return !this.isLoading; + } + + getFieldValidationClasses(field: FormControl): string { + let str = ''; + if (this.hasSubmitAttempt) { + if (field.invalid) { + str = 'is-invalid'; + } else if (field.value !== '') { + str = 'is-valid'; + } + } + return str; + } + + onSubmit(): void { + this.hasSubmitAttempt = true; + this.isLoading = true; + this.serverErrorMessage = ''; + + if (this.arf.invalid) { + this.isLoading = false; + // Do not submit form + return; + } + + const name = this.name.value!.trim(); + const email = this.email.value!.trim(); + const comments = this.comments.value!.trim(); + + // Combine country and institution + const country = this.country.value!.trim(); + const institution = this.institution.value!.trim(); + const combinedInstitution = `${institution}, ${country}`; + + const requestData: AccountCreateRequest = { + instructorEmail: email, + instructorName: name, + instructorInstitution: combinedInstitution, + }; + + if (comments) { + requestData.instructorComments = comments; + } + + this.accountService.createAccountRequest(requestData) + .pipe(finalize(() => { this.isLoading = false; })) + .subscribe({ + next: () => { + // Pass form input to parent to display confirmation + this.requestSubmissionEvent.emit({ + name, + institution, + country, + email, + comments, + }); + }, + error: (resp: ErrorMessageOutput) => { + this.serverErrorMessage = resp.error.message; + }, + }); + } +} diff --git a/src/web/app/pages-static/request-page/request-page.component.html b/src/web/app/pages-static/request-page/request-page.component.html index 6314f2563bd1..50b63e7dea1a 100644 --- a/src/web/app/pages-static/request-page/request-page.component.html +++ b/src/web/app/pages-static/request-page/request-page.component.html @@ -1,14 +1,59 @@

    - Request for an Account + Request for an Instructor Account

    -
    -

    - Cannot see the request form below? Click here. -

    - -
    -
    - The URL for the account request form is not set. +
    +
    +

    + Request for an instructor account using this form if you are an instructor and want to use TEAMMATES to manage peer evaluations and/or other feedback paths of your students. +

    +
    +
    +

    + Note: Students should not use this form to request for TEAMMATES accounts, as students do not need accounts to use TEAMMATES. Instead, TEAMMATES will email students (who have been added to TEAMMATES by a course instructor) an access link when there is a TEAMMATES session available for them to access. +

    + Back to home page + +
    +
    + +
    +
    +
    +
    +

    + Your request has been submitted successfully: +

    + + + + + + + + + + + + + + + + + + + + + + + +
    Full Name{{submittedFormData.name}}
    Institution{{submittedFormData.institution}}
    Country{{submittedFormData.country}}
    Email{{submittedFormData.email}}
    Comments + {{submittedFormData.comments}} + +
    +

    + We have sent an acknowledgement email to your email address {{submittedFormData.email}}. + Please check your email inbox or spam folder. + If you do not receive the acknowledgement email within 1 hour, please contact us. +

    +
    diff --git a/src/web/app/pages-static/request-page/request-page.component.scss b/src/web/app/pages-static/request-page/request-page.component.scss index e69de29bb2d1..3d1f1c83751c 100644 --- a/src/web/app/pages-static/request-page/request-page.component.scss +++ b/src/web/app/pages-static/request-page/request-page.component.scss @@ -0,0 +1,5 @@ +.empty-field-placeholder::after { + content: "(empty)"; + opacity: 0.5; + font-style: italic; +} diff --git a/src/web/app/pages-static/request-page/request-page.component.spec.ts b/src/web/app/pages-static/request-page/request-page.component.spec.ts index 3fdb7db2c448..9f4c3042247a 100644 --- a/src/web/app/pages-static/request-page/request-page.component.spec.ts +++ b/src/web/app/pages-static/request-page/request-page.component.spec.ts @@ -21,4 +21,30 @@ describe('RequestPageComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should render correctly before instructor declaration is done', () => { + component.isDeclarationDone = false; + component.submittedFormData = null; + fixture.detectChanges(); + expect(fixture).toMatchSnapshot(); + }); + + it('should render correctly after instructor declaration is done', () => { + component.isDeclarationDone = true; + component.submittedFormData = null; + fixture.detectChanges(); + expect(fixture).toMatchSnapshot(); + }); + + it('should render correctly after form is submitted', () => { + component.submittedFormData = { + name: 'Jane Smith', + institution: 'University of Example', + country: 'Example Republic', + email: 'js@exampleu.edu', + comments: '', + }; + fixture.detectChanges(); + expect(fixture).toMatchSnapshot(); + }); }); diff --git a/src/web/app/pages-static/request-page/request-page.component.ts b/src/web/app/pages-static/request-page/request-page.component.ts index 6cb307baa4be..7f0c2f155248 100644 --- a/src/web/app/pages-static/request-page/request-page.component.ts +++ b/src/web/app/pages-static/request-page/request-page.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; +import { InstructorRequestFormModel } from './instructor-request-form/instructor-request-form-model'; import { environment } from '../../../environments/environment'; /** @@ -13,6 +14,8 @@ import { environment } from '../../../environments/environment'; export class RequestPageComponent { accountRequestFormUrl: SafeResourceUrl | null; + isDeclarationDone: boolean = false; + submittedFormData: InstructorRequestFormModel | null = null; constructor(private sanitizer: DomSanitizer) { this.accountRequestFormUrl = environment.accountRequestFormUrl @@ -20,4 +23,11 @@ export class RequestPageComponent { : null; } + onDeclarationButtonClicked(): void { + this.isDeclarationDone = true; + } + + onRequestSubmitted(data: InstructorRequestFormModel): void { + this.submittedFormData = data; + } } diff --git a/src/web/app/pages-static/request-page/request-page.module.ts b/src/web/app/pages-static/request-page/request-page.module.ts index 9c16ee6fc4b6..12a9d337875d 100644 --- a/src/web/app/pages-static/request-page/request-page.module.ts +++ b/src/web/app/pages-static/request-page/request-page.module.ts @@ -1,7 +1,11 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; +import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'; +import { InstructorRequestFormComponent } from './instructor-request-form/instructor-request-form.component'; import { RequestPageComponent } from './request-page.component'; +import { TeammatesRouterModule } from '../../components/teammates-router/teammates-router.module'; const routes: Routes = [ { @@ -16,6 +20,7 @@ const routes: Routes = [ @NgModule({ declarations: [ RequestPageComponent, + InstructorRequestFormComponent, ], exports: [ RequestPageComponent, @@ -23,6 +28,9 @@ const routes: Routes = [ imports: [ CommonModule, RouterModule.forChild(routes), + TeammatesRouterModule, + ReactiveFormsModule, + NgbAlertModule, ], }) export class RequestPageModule { } diff --git a/src/web/data/developers.json b/src/web/data/developers.json index 842e83e13569..e5878d21caf4 100644 --- a/src/web/data/developers.json +++ b/src/web/data/developers.json @@ -211,14 +211,14 @@ "name": "André Alves", "username": "andremralves" }, - { - "name": "Andy Daehn", - "username": "andydaehn" - }, { "name": "Andy", "username": "Andy-W-Developer" }, + { + "name": "Andy Daehn", + "username": "andydaehn" + }, { "multiple": true, "name": "Ang Ji Kai", @@ -454,11 +454,6 @@ "name": "Chin Yong Wei", "username": "vertigogarden" }, - { - "multiple": true, - "name": "Ching Ming Yuan", - "username": "mingyuanc" - }, { "multiple": true, "name": "Chloe Stapleton", @@ -613,11 +608,6 @@ "name": "Divya Pandilla", "username": "pandilladivya" }, - { - "multiple": true, - "name": "Dominic Berzin", - "username": "domoberzin" - }, { "name": "Dominik Leszczenko", "username": "k4iz3n1994" @@ -659,6 +649,10 @@ { "username": "Emp923" }, + { + "name": "", + "username": "eraldprifti03" + }, { "name": "Eric Liu", "username": "ericluoliu" @@ -2258,11 +2252,6 @@ "name": "Truong Hoang Phuoc", "username": "hoangphuoc25" }, - { - "multiple": true, - "name": "Tye Jia Jun, Marques", - "username": "marquestye" - }, { "username": "u6867511" }, @@ -2368,11 +2357,6 @@ "name": "Wu Xiao Xiao", "username": "a0129998" }, - { - "multiple": true, - "name": "Xenos Fiorenzo Anong", - "username": "xenosf" - }, { "multiple": true, "name": "Xia Lu" @@ -2426,11 +2410,6 @@ "multiple": true, "name": "Yen Zi Shyun" }, - { - "multiple": true, - "name": "Yeo Di Sheng", - "username": "dishenggg" - }, { "name": "Yi Chen", "username": "g3chenyigmailcom" @@ -2507,11 +2486,6 @@ "name": "Zhu Yilun", "username": "yilun-zhu" }, - { - "multiple": true, - "name": "Zhu Yuanxi", - "username": "yuanxi1" - }, { "multiple": true, "name": "Zong Xun", @@ -2519,6 +2493,36 @@ } ], "committers": [ + { + "name": "Ching Ming Yuan", + "username": "mingyuanc", + "startPeriod": "Jan 2024" + }, + { + "name": "Dominic Berzin", + "username": "domoberzin", + "startPeriod": "Jan 2024" + }, + { + "name": "Tye Jia Jun, Marques", + "username": "marquestye", + "startPeriod": "Jan 2024" + }, + { + "name": "Xenos Fiorenzo Anong", + "username": "xenosf", + "startPeriod": "Jan 2024" + }, + { + "name": "Yeo Di Sheng", + "username": "dishenggg", + "startPeriod": "Jan 2024" + }, + { + "name": "Zhu Yuanxi", + "username": "yuanxi1", + "startPeriod": "Jan 2024" + }, { "name": "Jason Qiu", "username": "jasonqiu212", diff --git a/src/web/services/account.service.spec.ts b/src/web/services/account.service.spec.ts index 82a2e6a5985e..563315509116 100644 --- a/src/web/services/account.service.spec.ts +++ b/src/web/services/account.service.spec.ts @@ -84,19 +84,17 @@ describe('AccountService', () => { }); it('should execute DELETE on account request endpoint', () => { - service.deleteAccountRequest('testEmail', 'testInstitution'); + service.deleteAccountRequest('testId'); const paramMap: Record = { - instructoremail: 'testEmail', - instructorinstitution: 'testInstitution', + id: 'testId', }; expect(spyHttpRequestService.delete).toHaveBeenCalledWith(ResourceEndpoints.ACCOUNT_REQUEST, paramMap); }); it('should execute PUT on account request reset endpoint', () => { - service.resetAccountRequest('testEmail', 'testInstitution'); + service.resetAccountRequest('testId'); const paramMap: Record = { - instructoremail: 'testEmail', - instructorinstitution: 'testInstitution', + id: 'testId', }; expect(spyHttpRequestService.put).toHaveBeenCalledWith(ResourceEndpoints.ACCOUNT_REQUEST_RESET, paramMap); }); diff --git a/src/web/services/account.service.ts b/src/web/services/account.service.ts index f278335db558..35c9c2381115 100644 --- a/src/web/services/account.service.ts +++ b/src/web/services/account.service.ts @@ -2,8 +2,20 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { HttpRequestService } from './http-request.service'; import { ResourceEndpoints } from '../types/api-const'; -import { Account, Accounts, JoinLink, MessageOutput } from '../types/api-output'; -import { AccountCreateRequest } from '../types/api-request'; +import { + Account, + AccountRequest, + Accounts, + AccountRequests, + JoinLink, + MessageOutput, + AccountRequestStatus, +} from '../types/api-output'; +import { + AccountCreateRequest, + AccountRequestUpdateRequest, + AccountRequestRejectionRequest, +} from '../types/api-request'; /** * Handles account related logic provision @@ -29,7 +41,7 @@ export class AccountService { /** * Creates an account request by calling API. */ - createAccountRequest(request: AccountCreateRequest): Observable { + createAccountRequest(request: AccountCreateRequest): Observable { return this.httpRequestService.post(ResourceEndpoints.ACCOUNT_REQUEST, {}, request); } @@ -46,10 +58,9 @@ export class AccountService { /** * Deletes an account request by calling API. */ - deleteAccountRequest(email: string, institute: string): Observable { + deleteAccountRequest(id: string): Observable { const paramMap: Record = { - instructoremail: email, - instructorinstitution: institute, + id, }; return this.httpRequestService.delete(ResourceEndpoints.ACCOUNT_REQUEST, paramMap); } @@ -57,10 +68,9 @@ export class AccountService { /** * Resets an account request by calling API. */ - resetAccountRequest(email: string, institute: string): Observable { + resetAccountRequest(id: string): Observable { const paramMap: Record = { - instructoremail: email, - instructorinstitution: institute, + id, }; return this.httpRequestService.put(ResourceEndpoints.ACCOUNT_REQUEST_RESET, paramMap); } @@ -87,6 +97,44 @@ export class AccountService { return this.httpRequestService.put(ResourceEndpoints.ACCOUNT_RESET, paramMap); } + /** + * Approves account request by calling API + */ + approveAccountRequest(id: string, name: string, email: string, institute: string) + : Observable { + const paramMap: Record = { + id, + }; + const accountReqUpdateRequest : AccountRequestUpdateRequest = { + name, + email, + institute, + status: AccountRequestStatus.APPROVED, + }; + + return this.httpRequestService.put(ResourceEndpoints.ACCOUNT_REQUEST, paramMap, accountReqUpdateRequest); + } + + /** + * Edits an account request by calling API. + */ + editAccountRequest(id: string, name: string, email: string, institute: string, + status: AccountRequestStatus, comments: string) + : Observable { + const paramMap: Record = { + id, + }; + const accountReqUpdateRequest : AccountRequestUpdateRequest = { + name, + email, + institute, + status, + comments, + }; + + return this.httpRequestService.put(ResourceEndpoints.ACCOUNT_REQUEST, paramMap, accountReqUpdateRequest); + } + /** * Gets an account by calling API. */ @@ -107,4 +155,35 @@ export class AccountService { return this.httpRequestService.get(ResourceEndpoints.ACCOUNTS, paramMap); } + /** + * Gets account requests by calling API. + */ + getPendingAccountRequests(): Observable { + const paramMap = { + status: AccountRequestStatus.PENDING, + }; + + return this.httpRequestService.get(ResourceEndpoints.ACCOUNT_REQUESTS, paramMap); + } + + /** + * Rejects an account request by calling API. + */ + rejectAccountRequest(id: string, title?: string, body?: string): Observable { + let accountReqRejectRequest: AccountRequestRejectionRequest = {}; + + if (title !== undefined && body !== undefined) { + accountReqRejectRequest = { + reasonTitle: title, + reasonBody: body, + }; + } + + const paramMap: Record = { + id, + }; + + return this.httpRequestService.post(ResourceEndpoints.ACCOUNT_REQUEST_REJECT, paramMap, accountReqRejectRequest); + } + } diff --git a/src/web/services/log.service.ts b/src/web/services/log.service.ts index c61f88ce0c1a..3be61aa6d9f2 100644 --- a/src/web/services/log.service.ts +++ b/src/web/services/log.service.ts @@ -28,6 +28,8 @@ export class LogService { feedbackSessionName: string, studentEmail: string, logType: FeedbackSessionLogType, + feedbackSessionId?: string, + studentId?: string, }): Observable { const paramMap: Record = { courseid: queryParams.courseId, @@ -36,6 +38,14 @@ export class LogService { fsltype: queryParams.logType.toString(), }; + if (queryParams.feedbackSessionId) { + paramMap['fsid'] = queryParams.feedbackSessionId; + } + + if (queryParams.studentId) { + paramMap['studentid'] = queryParams.studentId; + } + return this.httpRequestService.post(ResourceEndpoints.SESSION_LOGS, paramMap); } @@ -49,6 +59,8 @@ export class LogService { studentEmail?: string, sessionName?: string, logType?: string, + studentId?: string, + sessionId?: string, }): Observable { const paramMap: Record = { courseid: queryParams.courseId, @@ -68,6 +80,14 @@ export class LogService { paramMap['fsltype'] = queryParams.logType; } + if (queryParams.studentId) { + paramMap['studentid'] = queryParams.studentId; + } + + if (queryParams.sessionId) { + paramMap['fsid'] = queryParams.sessionId; + } + return this.httpRequestService.get(ResourceEndpoints.SESSION_LOGS, paramMap); } diff --git a/src/web/services/search.service.spec.ts b/src/web/services/search.service.spec.ts index affbf259b4ab..f982f27cea99 100644 --- a/src/web/services/search.service.spec.ts +++ b/src/web/services/search.service.spec.ts @@ -13,6 +13,7 @@ import createSpyFromClass from '../test-helpers/create-spy-from-class'; import { ResourceEndpoints } from '../types/api-const'; import { AccountRequest, + AccountRequestStatus, Course, FeedbackSession, FeedbackSessionPublishStatus, @@ -184,11 +185,14 @@ describe('SearchService', () => { }; const mockAccountRequest: AccountRequest = { + id: '132efa02-b208-4195-a262-a8eae25ceb95', registrationKey: 'regkey', createdAt: 1585487897502, name: 'Test Instructor', institute: 'Test Institute', email: 'test@example.com', + comments: 'This is a test account request', + status: AccountRequestStatus.APPROVED, }; beforeEach(() => { @@ -294,9 +298,14 @@ describe('SearchService', () => { it('should join account requests accurately when timezone can be guessed and instructor is registered', () => { jest.spyOn(timezoneService, 'guessTimezone').mockReturnValue('Asia/Singapore'); - const accountRequest: AccountRequest = { ...mockAccountRequest, registeredAt: 1685487897502 }; + const accountRequest: AccountRequest = { + ...mockAccountRequest, + registeredAt: 1685487897502, + status: AccountRequestStatus.REGISTERED, + }; const result: AccountRequestSearchResult = service.joinAdminAccountRequest(accountRequest); + expect(result.id).toBe('132efa02-b208-4195-a262-a8eae25ceb95'); expect(result.email).toBe('test@example.com'); expect(result.institute).toBe('Test Institute'); expect(result.name).toBe('Test Instructor'); diff --git a/src/web/services/search.service.ts b/src/web/services/search.service.ts index 7542da645cc9..be19f3cd2772 100644 --- a/src/web/services/search.service.ts +++ b/src/web/services/search.service.ts @@ -11,6 +11,7 @@ import { ResourceEndpoints } from '../types/api-const'; import { AccountRequest, AccountRequests, + AccountRequestStatus, Course, FeedbackSession, FeedbackSessions, Instructor, @@ -299,6 +300,7 @@ export class SearchService { joinAdminAccountRequest(accountRequest: AccountRequest): AccountRequestSearchResult { let accountRequestResult: AccountRequestSearchResult = { + id: '', name: '', email: '', institute: '', @@ -306,16 +308,22 @@ export class SearchService { registeredAtText: '', registrationLink: '', showLinks: false, + status: AccountRequestStatus.PENDING, + comments: '', }; - const { registrationKey, createdAt, registeredAt, name, institute, email }: AccountRequest = accountRequest; + const { + id, registrationKey, createdAt, registeredAt, + name, institute, email, status, comments, + }: AccountRequest = accountRequest; const timezone: string = this.timezoneService.guessTimezone() || 'UTC'; accountRequestResult.createdAtText = this.formatTimestampAsString(createdAt, timezone); accountRequestResult.registeredAtText = registeredAt ? this.formatTimestampAsString(registeredAt, timezone) : null; + accountRequestResult.comments = comments || ''; const registrationLink: string = this.linkService.generateAccountRegistrationLink(registrationKey); - accountRequestResult = { ...accountRequestResult, name, email, institute, registrationLink }; + accountRequestResult = { ...accountRequestResult, id, name, email, institute, registrationLink, status }; return accountRequestResult; } @@ -464,13 +472,16 @@ export interface AdminSearchResult { * Search results for account requests from the admin endpoint. */ export interface AccountRequestSearchResult { + id: string; name: string; email: string; + status: AccountRequestStatus; institute: string; createdAtText: string; registeredAtText: string | null; registrationLink: string; showLinks: boolean; + comments: string; } /** diff --git a/src/web/types/const.spec.ts b/src/web/types/const.spec.ts index f5f403835ec6..6bc4e7112ec2 100644 --- a/src/web/types/const.spec.ts +++ b/src/web/types/const.spec.ts @@ -1,4 +1,4 @@ -import { ApiConst } from './api-const'; +import { ApiConst, ApiStringConst } from './api-const'; import { FeedbackQuestionType } from './api-output'; import { DEFAULT_INSTRUCTOR_PRIVILEGE, @@ -68,6 +68,12 @@ describe('Constants', () => { expect(typeof ApiConst.NO_VALUE).toEqual('number'); }); + // Here we test that the constants are strings + it('should generate string constants correctly', () => { + expect(typeof ApiStringConst.EMAIL_REGEX).toEqual('string'); + expect(() => new RegExp(ApiStringConst.EMAIL_REGEX)).not.toThrow(); + }); + // Here we test that: // 1. The string is parseable to JSON // 2. The question type is correct diff --git a/src/web/types/form-validator.ts b/src/web/types/form-validator.ts index c2f44cf5beb0..ea18815ca5dd 100644 --- a/src/web/types/form-validator.ts +++ b/src/web/types/form-validator.ts @@ -1,4 +1,4 @@ -import { ApiConst } from './api-const'; +import { ApiConst, ApiStringConst } from './api-const'; /** * Represents the root FormValidator object of all form fields. @@ -33,4 +33,35 @@ export enum FormValidator { * Max length for the 'E-mail Address` field. */ EMAIL_MAX_LENGTH = ApiConst.EMAIL_MAX_LENGTH, + + /** + * Regex used to verify emails in the back-end. + */ + EMAIL_REGEX = ApiStringConst.EMAIL_REGEX, + + /** + * Regex used to verify names. + * + * Based on back-end's `FieldValidator.REGEX_NAME`. + * The back-end regex is not converted to use here as the pattern syntax is not accepted in JS. + */ + NAME_REGEX = '^[a-zA-Z0-9][^|%]*$', + + /** + * Regex used to verify country names. + * + * Based on back-end's `FieldValidator.REGEX_NAME`, but without needing to start with alphanumeric + * as the country is added to the end of the combined institute string. + */ + COUNTRY_REGEX = '^[^|%]*$', + + /** + * Max length for institution name in account request. (to be combined with country) + */ + INSTITUTION_NAME_MAX_LENGTH = 86, + + /** + * Max length for country in account request. (to be combined with institution name) + */ + COUNTRY_NAME_MAX_LENGTH = 40, } diff --git a/static-analysis/teammates-checkstyle.xml b/static-analysis/teammates-checkstyle.xml index 8fa0d80ea19e..8e58b6f8f470 100644 --- a/static-analysis/teammates-checkstyle.xml +++ b/static-analysis/teammates-checkstyle.xml @@ -145,13 +145,13 @@ - + - + diff --git a/static-analysis/teammates-pmd.xml b/static-analysis/teammates-pmd.xml index a75119b031e4..d5529f08fd92 100644 --- a/static-analysis/teammates-pmd.xml +++ b/static-analysis/teammates-pmd.xml @@ -20,6 +20,8 @@ + + @@ -31,11 +33,13 @@ - + + + + + + - - @@ -148,7 +152,6 @@ - @@ -157,7 +160,7 @@ - + @@ -177,10 +180,12 @@ + +