diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000000..1f33912b41 --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,62 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + REPO: ${{ github.repository }} + PR NUMBER: ${{ github.event.pull_request.number }} + + Please review this pull request and provide inline feedback using the GitHub review system. + + Review focus areas: + - Code quality and best practices + - Potential bugs or issues + - Performance considerations + - Security concerns + - Test coverage + + Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. + + Instructions: + 1. Use the GitHub MCP tools to fetch the PR diff + 2. Add inline comments using the appropriate MCP tools for each specific piece of feedback on particular lines + 3. Submit the review with event type 'COMMENT' (not 'REQUEST_CHANGES') to publish as non-blocking feedback + + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options + claude_args: '--allowedTools "mcp__github__create_pending_pull_request_review,mcp__github__add_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"' + diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000000..2b6c87da48 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' + diff --git a/.github/workflows/unit-test-coverage.yml b/.github/workflows/unit-test-coverage.yml new file mode 100644 index 0000000000..e96fb58f69 --- /dev/null +++ b/.github/workflows/unit-test-coverage.yml @@ -0,0 +1,258 @@ +name: Unit Test Coverage + +on: + pull_request: + branches: [ master ] + +jobs: + test-pr: + runs-on: ubuntu-latest + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Setup open source build + run: ./open_source.sh + + - name: Run unit tests and generate coverage for PR + run: | + # Run tests in parallel - Gradle will handle parallelization + ./gradle/gradlew -p apps :student:testQaDebugUnitTest :teacher:testQaDebugUnitTest :pandautils:testDebugUnitTest --parallel + + # Copy exec files to expected locations for jacoco.gradle + mkdir -p apps/student/build/jacoco apps/teacher/build/jacoco libs/pandautils/build/jacoco + + cp apps/student/build/outputs/unit_test_code_coverage/qaDebugUnitTest/testQaDebugUnitTest.exec apps/student/build/jacoco/testQaDebugUnitTest.exec 2>/dev/null || echo "Student exec not found" + cp apps/teacher/build/outputs/unit_test_code_coverage/qaDebugUnitTest/testQaDebugUnitTest.exec apps/teacher/build/jacoco/testQaDebugUnitTest.exec 2>/dev/null || echo "Teacher exec not found" + cp libs/pandautils/build/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec libs/pandautils/build/jacoco/testDebugUnitTest.exec 2>/dev/null || echo "Pandautils exec not found" + + # Generate JaCoCo reports in parallel + ./gradle/gradlew -p apps :student:jacocoReport :teacher:jacocoReport :pandautils:jacocoReport --parallel + continue-on-error: false + + - name: Upload PR coverage reports + uses: actions/upload-artifact@v4 + with: + name: pr-coverage + path: | + apps/student/build/reports/jacoco/jacocoReport/jacocoReport.csv + apps/teacher/build/reports/jacoco/jacocoReport/jacocoReport.csv + libs/pandautils/build/reports/jacoco/jacocoReport/jacocoReport.csv + retention-days: 1 + + test-master: + runs-on: ubuntu-latest + + steps: + - name: Checkout master branch + uses: actions/checkout@v4 + with: + ref: master + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Setup open source build + run: ./open_source.sh + + - name: Run unit tests and generate coverage for master + run: | + # Run tests in parallel - Gradle will handle parallelization + ./gradle/gradlew -p apps :student:testQaDebugUnitTest :teacher:testQaDebugUnitTest :pandautils:testDebugUnitTest --parallel + + # Copy exec files to expected locations for jacoco.gradle + mkdir -p apps/student/build/jacoco apps/teacher/build/jacoco libs/pandautils/build/jacoco + + cp apps/student/build/outputs/unit_test_code_coverage/qaDebugUnitTest/testQaDebugUnitTest.exec apps/student/build/jacoco/testQaDebugUnitTest.exec 2>/dev/null || echo "Student exec not found" + cp apps/teacher/build/outputs/unit_test_code_coverage/qaDebugUnitTest/testQaDebugUnitTest.exec apps/teacher/build/jacoco/testQaDebugUnitTest.exec 2>/dev/null || echo "Teacher exec not found" + cp libs/pandautils/build/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec libs/pandautils/build/jacoco/testDebugUnitTest.exec 2>/dev/null || echo "Pandautils exec not found" + + # Generate JaCoCo reports in parallel + ./gradle/gradlew -p apps :student:jacocoReport :teacher:jacocoReport :pandautils:jacocoReport --parallel + continue-on-error: true + + - name: Upload master coverage reports + uses: actions/upload-artifact@v4 + with: + name: master-coverage + path: | + apps/student/build/reports/jacoco/jacocoReport/jacocoReport.csv + apps/teacher/build/reports/jacoco/jacocoReport/jacocoReport.csv + libs/pandautils/build/reports/jacoco/jacocoReport/jacocoReport.csv + retention-days: 1 + + coverage-report: + runs-on: ubuntu-latest + needs: [test-pr, test-master] + if: always() + + steps: + - name: Download PR coverage + uses: actions/download-artifact@v4 + with: + name: pr-coverage + path: coverage-reports/pr + + - name: Download master coverage + uses: actions/download-artifact@v4 + with: + name: master-coverage + path: coverage-reports/master + continue-on-error: true + + - name: Reorganize coverage files + run: | + # Reorganize downloaded artifacts to expected structure + mkdir -p coverage-reports/pr coverage-reports/master + + # PR coverage + find coverage-reports/pr -name "jacocoReport.csv" -path "*/student/*" -exec cp {} coverage-reports/pr/student.csv \; 2>/dev/null || echo "Student PR coverage not found" + find coverage-reports/pr -name "jacocoReport.csv" -path "*/teacher/*" -exec cp {} coverage-reports/pr/teacher.csv \; 2>/dev/null || echo "Teacher PR coverage not found" + find coverage-reports/pr -name "jacocoReport.csv" -path "*/pandautils/*" -exec cp {} coverage-reports/pr/pandautils.csv \; 2>/dev/null || echo "Pandautils PR coverage not found" + + # Master coverage + find coverage-reports/master -name "jacocoReport.csv" -path "*/student/*" -exec cp {} coverage-reports/master/student.csv \; 2>/dev/null || echo "Student master coverage not found" + find coverage-reports/master -name "jacocoReport.csv" -path "*/teacher/*" -exec cp {} coverage-reports/master/teacher.csv \; 2>/dev/null || echo "Teacher master coverage not found" + find coverage-reports/master -name "jacocoReport.csv" -path "*/pandautils/*" -exec cp {} coverage-reports/master/pandautils.csv \; 2>/dev/null || echo "Pandautils master coverage not found" + + - name: Calculate coverage delta + id: coverage + run: | + python3 << 'EOF' | tee coverage-report.txt + import csv + import os + from pathlib import Path + + def parse_jacoco_csv(file_path): + """Parse JaCoCo CSV and return instruction coverage percentage""" + if not Path(file_path).exists(): + return None + + total_missed = 0 + total_covered = 0 + + with open(file_path, 'r') as f: + reader = csv.DictReader(f) + for row in reader: + total_missed += int(row['INSTRUCTION_MISSED']) + total_covered += int(row['INSTRUCTION_COVERED']) + + if total_missed + total_covered == 0: + return 0.0 + + return (total_covered / (total_missed + total_covered)) * 100 + + modules = ['student', 'teacher', 'pandautils'] + results = [] + + print("## 📊 Code Coverage Report\n") + + overall_pr_coverage = [] + overall_master_coverage = [] + + for module in modules: + pr_file = f'coverage-reports/pr/{module}.csv' + master_file = f'coverage-reports/master/{module}.csv' + + pr_cov = parse_jacoco_csv(pr_file) + master_cov = parse_jacoco_csv(master_file) + + if pr_cov is not None and master_cov is not None: + delta = pr_cov - master_cov + emoji = '✅' if delta >= 0 else 'âš ī¸' + sign = '+' if delta >= 0 else '' + + print(f"### {emoji} {module.capitalize()}") + print(f"- **PR Coverage:** {pr_cov:.2f}%") + print(f"- **Master Coverage:** {master_cov:.2f}%") + print(f"- **Delta:** {sign}{delta:.2f}%\n") + + overall_pr_coverage.append(pr_cov) + overall_master_coverage.append(master_cov) + elif pr_cov is not None: + print(f"### â„šī¸ {module.capitalize()}") + print(f"- **PR Coverage:** {pr_cov:.2f}%") + print(f"- **Master Coverage:** N/A\n") + else: + print(f"### âš ī¸ {module.capitalize()}") + print(f"- Coverage data not available\n") + + if overall_pr_coverage and overall_master_coverage: + avg_pr = sum(overall_pr_coverage) / len(overall_pr_coverage) + avg_master = sum(overall_master_coverage) / len(overall_master_coverage) + overall_delta = avg_pr - avg_master + + print("---") + print(f"### 📈 Overall Average") + print(f"- **PR Coverage:** {avg_pr:.2f}%") + print(f"- **Master Coverage:** {avg_master:.2f}%") + sign = '+' if overall_delta >= 0 else '' + print(f"- **Delta:** {sign}{overall_delta:.2f}%") + + # Set output for potential failure condition + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f"delta={overall_delta}\n") + + EOF + + - name: Comment PR (sticky) + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const output = fs.readFileSync('coverage-report.txt', 'utf8'); + const marker = ''; + const body = marker + '\n' + output; + + // Find existing coverage comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existingComment = comments.find(comment => + comment.body.includes(marker) + ); + + if (existingComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: body + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + }); + } + + # Optional: Fail if coverage decreases by more than 1% + # - name: Check coverage threshold + # if: steps.coverage.outputs.delta < -1.0 + # run: | + # echo "Coverage decreased by more than 1%" + # exit 1 \ No newline at end of file diff --git a/PULL_REQUEST_TEMPLATE b/PULL_REQUEST_TEMPLATE index b3cb7a93d5..aa3344b325 100644 --- a/PULL_REQUEST_TEMPLATE +++ b/PULL_REQUEST_TEMPLATE @@ -22,5 +22,6 @@ release note: - [ ] Run E2E test suite - [ ] Tested in dark mode - [ ] Tested in light mode +- [ ] Test in landscape mode and/or tablet - [ ] A11y checked - [ ] Approve from product diff --git a/apps/.github/copilot-instructions.md b/apps/.github/copilot-instructions.md new file mode 100644 index 0000000000..b2ed7148b5 --- /dev/null +++ b/apps/.github/copilot-instructions.md @@ -0,0 +1,68 @@ +# GitHub Copilot Preferences + +## Project Context + +- Canvas is a learning management system with multiple Android apps (Student, Teacher, Parent) +- The canvas-android project consists of multiple packages managed with Gradle +- The apps share a common :pandautils module for shared code +- There is a standalone module for each app (student, teacher, parent) +- The apps are written in Kotlin and use a mix of views with xml and Jetpack Compose for UI +- The apps use Retrofit and OkHttp for networking +- The apps use Room for local database storage +- The apps use Dagger Hilt for dependency injection +- The apps use Coroutines and Flow for asynchronous programming +- The apps use JUnit and Espresso for testing +- The apps follow MVVM architecture pattern +- The apps use Material Design components for UI +- The apps support multiple screen sizes and orientations +- The apps support dark mode +- The apps support multiple languages and localization + +## Response Preferences + +- Be concise and prioritize code examples +- Suggest Kotlin solutions with proper typing +- When suggesting code, ensure it follows existing project patterns and conventions +- When explaining code, focus on implementation details and potential edge cases +- When suggesting libraries or tools, ensure they are compatible with existing project dependencies and architecture +- When suggesting architectural changes, consider the impact on existing code and dependencies +- When suggesting UI changes, consider accessibility and responsiveness +- When suggesting testing strategies, consider existing test coverage and frameworks used +- Use Kotlin, Jetpack Compose and Kotlin Coroutines best practices appropriately +- Reference existing project patterns when suggesting new implementations +- Always use test you can see the output of. + +## Tool Usage Preferences + +- Prefer searching through codebase before suggesting solutions +- Use error checking after code edits +- When given a ticket identifier, use Atlassian MCP server to gather information about the task +- When given a Figma link, use Figma Desktop MCP server to gather design details + +## Code Style Preferences +- Follow existing project code style and conventions +- Use consistent indentation and spacing +- Use descriptive variable and function names +- Prefer immutability where possible +- Use Kotlin idioms and best practices +- Avoid unnecessary complexity +- Ensure code is modular and reusable +- Follow SOLID principles +- Ensure proper error handling and logging +- Write unit tests for new functionality +- Write integration tests for new functionality +- Write UI tests for new functionality +- Ensure tests are isolated and repeatable +- Use mocking frameworks for dependencies in tests +- Ensure code coverage is maintained or improved with new changes +- When suggesting code snippets, ensure they are complete and can be directly used or easily integrated +- DO NOT add comments or documentation unless its specifically requested +- Code should be self-explanatory without inline comments +- When generating test files, only include license headers and the actual test code without explanatory comments + +## Implementation Preferences +- Follow project's component structure and naming conventions +- Use existing utility functions and shared components when possible +- Ensure the code compiles and runs without errors +- When writing tests, make sure the tests pass +- When you are asked to write tests, ensure they are written in the same manner as existing tests in the project \ No newline at end of file diff --git a/apps/CLAUDE.md b/apps/CLAUDE.md new file mode 100644 index 0000000000..f7db45f50d --- /dev/null +++ b/apps/CLAUDE.md @@ -0,0 +1,243 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Canvas Android is a multi-app learning management system project with three main applications (Student, Teacher, Parent) sharing common libraries. The apps are built with Kotlin, Jetpack Compose (modern UI) and XML layouts (legacy), following MVVM architecture with Dagger Hilt for dependency injection. + +**Main Applications:** +- `student/` - Canvas Student app for students +- `teacher/` - Canvas Teacher app for teachers +- `parent/` - Canvas Parent app for parents (native version; there's also a Flutter version at `flutter_parent/`) + +**Shared Libraries (in `../libs/`):** +- `pandautils/` - Core shared library with common code, features, compose components, and utilities +- `canvas-api-2/` - Canvas LMS API client using Retrofit/OkHttp +- `login-api-2/` - Authentication and login flows +- `annotations/` - PSPDFKit wrapper for PDF annotation handling +- `rceditor/` - Rich content editor wrapper +- `interactions/` - Navigation interactions +- `horizon/` - Career experience features +- `pandares/` - Shared resources + +**Testing Libraries (in `../automation/`):** +- `espresso/` - UI testing framework built on Espresso +- `dataseedingapi/` - gRPC wrapper for Canvas data seeding in tests + +## Build Commands + +Run from repository root (`canvas-android/`), not the `apps/` directory: + +```bash +# Build Student app (dev debug variant) +./gradle/gradlew -p apps :student:assembleDevDebug + +# Build Teacher app (dev debug variant) +./gradle/gradlew -p apps :teacher:assembleDevDebug + +# Build Parent app (dev debug variant) +./gradle/gradlew -p apps :parent:assembleDevDebug + +# Build all apps +./gradle/gradlew -p apps assembleAllApps + +# Clean build +./gradle/gradlew -p apps clean +``` + +## Running Tests + +**Unit Tests:** +1. Set Build Variant to `qaDebug` in Android Studio +2. Run tests by clicking the play button next to test cases/classes +3. Or via command line: +```bash +./gradle/gradlew -p apps :student:testQaDebugUnitTest +./gradle/gradlew -p apps :teacher:testQaDebugUnitTest +./gradle/gradlew -p apps :parent:testQaDebugUnitTest +``` + +**Shared Library Tests:** +```bash +# Test a specific module +./gradle/gradlew -p apps :pandautils:testDebugUnitTest + +# Test specific class +./gradle/gradlew -p apps :pandautils:testDebugUnitTest --tests "com.instructure.pandautils.features.discussion.router.DiscussionRouterViewModelTest.*" + +# Force re-run tests (ignore cache) +./gradle/gradlew -p apps :pandautils:testDebugUnitTest --rerun-tasks +``` + +**Instrumentation/Espresso Tests:** +```bash +./gradle/gradlew -p apps :student:connectedQaDebugAndroidTest +./gradle/gradlew -p apps :teacher:connectedQaDebugAndroidTest +``` + +**Single Test:** +```bash +# Unit test +./gradle/gradlew -p apps :student:testQaDebugUnitTest --tests "com.instructure.student.SpecificTest" + +# Instrumentation test +./gradle/gradlew -p apps :student:connectedQaDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.instructure.student.ui.SpecificTest +``` + +## Architecture + +### Feature Organization + +Features follow a modular structure within each app under `src/main/java/com/instructure/{app}/features/{feature}/`: + +``` +features/ + └── featurename/ + ├── FeatureScreen.kt # Compose UI (modern) + ├── FeatureFragment.kt # Fragment (legacy) + ├── FeatureViewModel.kt # Business logic + ├── FeatureRepository.kt # Data layer + └── FeatureUiState.kt # UI state models +``` + +**Test Structure:** +- Unit tests: `src/test/java/com/instructure/{app}/features/{feature}/` +- Instrumentation tests: `src/androidTest/java/com/instructure/{app}/ui/{feature}/` + +### MVVM Pattern + +- **ViewModels** handle business logic and state management using Kotlin Flows/LiveData +- **Repositories** abstract data sources (API, local database, etc.) +- **UI Layer** - Jetpack Compose for new features, XML + Data Binding for legacy +- **Dependency Injection** - Dagger Hilt modules in `di/` directories + +### Key Technologies + +- **UI**: Jetpack Compose (modern), XML layouts + Data Binding (legacy), Material Design 3 +- **Networking**: Retrofit 3.0, OkHttp 5.1, Apollo GraphQL 4.3 +- **Database**: Room 2.8 with Coroutines +- **DI**: Dagger Hilt 2.57 +- **Async**: Kotlin Coroutines 1.9, Flow, LiveData +- **Testing**: JUnit 4, Mockk, Robolectric, Espresso, Compose UI Testing +- **Other**: Mobius (for some features), WorkManager, Firebase (Crashlytics, Messaging) + +### Build Configuration + +- **Build Tools**: AGP 8.13, Kotlin 2.2, KSP 2.0 +- **SDK**: compileSdk 35, minSdk 28, targetSdk 35 +- **Java**: Version 17 +- **Build Variants**: + - Flavors: `dev`, `qa`, `prod` + - Types: `debug`, `debugMinify`, `release` + - Common: `devDebug` (development), `qaDebug` (testing) + +Dependencies are centralized in `buildSrc/src/main/java/GlobalDependencies.kt` with `Versions`, `Libs`, and `Plugins` objects. + +## Development Guidelines + +### Code Style +- Use Kotlin idioms and best practices +- Prefer immutability where possible +- Follow existing project patterns and conventions +- Self-documenting code without inline comments unless specifically requested +- Use descriptive variable and function names +- **Companion objects**: Place companion objects at the bottom of the class, following Kotlin style guides. For simple private constants used only within a file, consider using top-level constants instead + +### Component Patterns +- Use existing utility functions and shared components from `pandautils` +- Follow project's component structure and naming conventions +- Prefer Repository pattern for data access +- Use Hilt for dependency injection +- New UI features should use Jetpack Compose +- Legacy features may use XML + Data Binding + +### Testing Patterns +- Write unit tests in the same manner as existing tests (e.g., check `student/src/test/`) +- Write instrumentation tests in the same manner as existing tests (e.g., check `student/src/androidTest/`) +- Mock dependencies with Mockk +- Use test doubles for repositories in ViewModel tests +- Espresso tests should use page object pattern from `:espresso` module +- Ensure tests are isolated and repeatable + +### Module Dependencies +- Apps depend on shared libraries (`:pandautils`, `:canvas-api-2`, etc.) +- Shared libraries are in `../libs/` relative to `apps/` +- Canvas API models and endpoints are in `:canvas-api-2` +- Common utilities, dialogs, and base classes are in `:pandautils` + +### Router Pattern +The project uses a Router pattern for navigation between features: +- **Interface Definition**: Router interfaces defined in `pandautils` (e.g., `DiscussionRouter`, `CalendarRouter`) +- **App Implementation**: Each app implements the router interface (e.g., `StudentDiscussionRouter`, `TeacherDiscussionRouter`) +- **Dependency Injection**: Routers are injected via Hilt into Fragments/ViewModels +- **Usage**: ViewModels emit actions that are handled by Fragments, which delegate to app-specific routers +- **Routing Methods**: Routers use `RouteMatcher.route()` to navigate to other fragments using `Route` objects +- **Example Flow**: ViewModel → Action → Fragment.handleAction() → Router.routeTo*() → RouteMatcher.route() + +## Device Deployment + +**Install to Connected Device:** +```bash +# Install Student app +./gradle/gradlew -p apps :student:installDevDebug + +# Install Teacher app +./gradle/gradlew -p apps :teacher:installDevDebug + +# Install Parent app +./gradle/gradlew -p apps :parent:installDevDebug +``` + +**Launch App:** +```bash +# Check connected devices +adb devices + +# Launch using monkey (works with any launcher activity) +adb shell monkey -p com.instructure.candroid -c android.intent.category.LAUNCHER 1 +adb shell monkey -p com.instructure.teacher -c android.intent.category.LAUNCHER 1 +adb shell monkey -p com.instructure.parentapp -c android.intent.category.LAUNCHER 1 +``` + +**Common ADB Commands:** +```bash +# View logs +adb logcat | grep "candroid" + +# Clear app data +adb shell pm clear com.instructure.candroid + +# Uninstall app +adb uninstall com.instructure.candroid +``` + +## Additional Context + +### Initial Setup +Before first build, run from repository root: +```bash +./open_source.sh +``` + +This sets up Flutter SDK (if working with Flutter Parent) and other initial configuration. + +### ProGuard +Each app has ProGuard rules in `{app}/proguard-rules.txt` + +### Private Data +The project uses `PrivateData.merge()` to inject private configuration (API keys, tokens) from `android-vault/private-data/`. These are not in version control. + +### Localization +Apps support multiple languages. Translation tags are scanned at build time via `LocaleScanner.getAvailableLanguageTags()`. + +### Pull Requests +When creating a pull request, use the template located at `/PULL_REQUEST_TEMPLATE` in the repository root. The template includes: +- Test plan description +- Issue references (refs:) +- Impact scope (affects:) +- Release note +- Screenshots table (Before/After) +- Checklist (E2E tests, dark/light mode, landscape/tablet, accessibility, product approval) + +Use `gh pr create` with the template to create PRs from the command line. \ No newline at end of file diff --git a/apps/build.gradle b/apps/build.gradle index a0b3f6aaa8..5d6de59820 100644 --- a/apps/build.gradle +++ b/apps/build.gradle @@ -34,6 +34,7 @@ buildscript { classpath Plugins.FIREBASE_CRASHLYTICS if (project.coverageEnabled) { classpath Plugins.JACOCO_ANDROID } classpath Plugins.HILT + classpath Plugins.KSP } } @@ -50,10 +51,40 @@ allprojects { username pspdfMavenUser password pspdfMavenPass } - url 'https://customers.pspdfkit.com/maven/' + url 'https://my.nutrient.io/maven' } maven { url "https://maven.google.com/" } } + + plugins.withType(com.android.build.gradle.BasePlugin) { + android { + packaging { + resources { + pickFirsts += [ + 'META-INF/INDEX.LIST', + 'META-INF/io.netty.versions.properties' + ] + merges += [ + 'META-INF/LICENSE*', + 'META-INF/NOTICE*', + 'META-INF/DEPENDENCIES*' + ] + excludes += [ + 'META-INF/DEPENDENCIES', + 'META-INF/LICENSE', + 'META-INF/LICENSE.txt', + 'META-INF/LICENSE.md', + 'META-INF/NOTICE', + 'META-INF/NOTICE.txt', + 'META-INF/NOTICE.md', + 'META-INF/maven/**', + 'META-INF/*.kotlin_module', + 'META-INF/services/javax.annotation.processing.Processor' + ] + } + } + } + } } task assembleAllApps() { @@ -72,4 +103,3 @@ configurations.all{ } } } - diff --git a/apps/buildSrc/src/main/groovy/TimingsListener.groovy b/apps/buildSrc/src/main/groovy/TimingsListener.groovy index 7d14d278c8..fcef0f946e 100644 --- a/apps/buildSrc/src/main/groovy/TimingsListener.groovy +++ b/apps/buildSrc/src/main/groovy/TimingsListener.groovy @@ -21,7 +21,6 @@ class TimingsListener implements TaskExecutionListener, BuildListener { buildStartTime = System.nanoTime() } - @Override void beforeExecute(Task task) { startTime = System.nanoTime() @@ -39,8 +38,8 @@ class TimingsListener implements TaskExecutionListener, BuildListener { // Compute build time def totalBuildTimeMs = TimeUnit.MILLISECONDS.convert(System.nanoTime() - buildStartTime, TimeUnit.NANOSECONDS) - // Grab the Splunk-mobile token from Bitrise - def splunkToken = System.getenv("SPLUNK_MOBILE_TOKEN") + // Grab the Observe-mobile token from Bitrise + def observeToken = System.getenv("OBSERVE_MOBILE_TOKEN") // Let's abort early if (1) the build failed, or (2) we're not on bitrise if(result.failure != null) { @@ -48,7 +47,7 @@ class TimingsListener implements TaskExecutionListener, BuildListener { return } - if(splunkToken == null || splunkToken.isEmpty()) { + if(observeToken == null || observeToken.isEmpty()) { println("Build report logic aborting because we're not on bitrise") return } @@ -118,8 +117,7 @@ class TimingsListener implements TaskExecutionListener, BuildListener { } println("file name=${file.path} length=${file.length()}") - - // Construct the JSON payload for our "buildComplete" event + // Construct the JSON eventPayload for our "buildComplete" event def payloadBuilder = new groovy.json.JsonBuilder() payloadBuilder buildTime: totalBuildTimeMs, gradleTasks: startTaskNames, @@ -131,16 +129,20 @@ class TimingsListener implements TaskExecutionListener, BuildListener { bitriseBuildNumber: bitriseBuildNumber, topTasks: top10 - // Create the event payload. Change key/value in top 10 tasks to task/ms. - def payload = payloadBuilder.toString().replaceAll("\"key\"", "\"task\"").replaceAll("\"value\"", "\"ms\"") + // Create the event eventPayload. Change key/value in top 10 tasks to task/ms. + def eventPayload = payloadBuilder.toString().replaceAll("\"key\"", "\"task\"").replaceAll("\"value\"", "\"ms\"") + + println("event eventPayload: $eventPayload") - println("event payload: $payload") + //Wrap it in the "data" object for Observe + def payload="{\"data\": $eventPayload}" // Let's issue our curl command to emit our data refProject.exec { executable "curl" - args "-k", "https://http-inputs-inst.splunkcloud.com:443/services/collector", "-H", "Authorization: Splunk $splunkToken", - "-d", "{\"sourcetype\" : \"mobile-android-build\", \"event\" : $payload}" + args "-k", "https://103443579803.collect.observeinc.com/v1/http", "-H", "Authorization: Bearer $observeToken", + "-H", "Content-Type: application/json", + "-d", "$payload" } } diff --git a/apps/buildSrc/src/main/java/GlobalDependencies.kt b/apps/buildSrc/src/main/java/GlobalDependencies.kt index affae9257d..d922ebaa3c 100644 --- a/apps/buildSrc/src/main/java/GlobalDependencies.kt +++ b/apps/buildSrc/src/main/java/GlobalDependencies.kt @@ -8,7 +8,7 @@ object Versions { /* Build/tooling */ const val ANDROID_GRADLE_TOOLS = "8.6.1" - const val BUILD_TOOLS = "34.0.0" + const val BUILD_TOOLS = "35.0.0" /* Testing */ const val JUNIT = "4.13.2" @@ -18,32 +18,34 @@ object Versions { /* Kotlin */ const val KOTLIN = "2.0.21" const val KOTLIN_COROUTINES = "1.9.0" + const val KSP = "2.0.21-1.0.27" /* Google, Play Services */ - const val GOOGLE_SERVICES = "4.4.2" + const val GOOGLE_SERVICES = "4.4.3" /* Others */ - const val APOLLO = "4.1.1" - const val PSPDFKIT = "2024.3.1" + const val APOLLO = "4.3.3" + const val NUTRIENT = "10.7.0" const val PHOTO_VIEW = "2.3.0" const val MOBIUS = "1.2.1" - const val HILT = "2.52" - const val HILT_ANDROIDX = "1.2.0" - const val LIFECYCLE = "2.8.6" - const val FRAGMENT = "1.8.4" - const val WORK_MANAGER = "2.9.1" - const val GLIDE_VERSION = "4.16.0" + const val HILT = "2.57.2" + const val HILT_ANDROIDX = "1.3.0" + const val LIFECYCLE = "2.9.4" + const val FRAGMENT = "1.8.9" + const val WORK_MANAGER = "2.10.5" + const val GLIDE_VERSION = "5.0.5" const val RETROFIT = "2.11.0" const val OKHTTP = "4.12.0" - const val ROOM = "2.6.1" - const val HAMCREST = "2.2" - const val NAVIGATION = "2.8.3" - const val MEDIA3 = "1.6.1" - const val DATASTORE = "1.1.1" - const val LOTTIE = "6.5.2" - const val ENCRYPTED_SHARED_PREFERENCES = "1.0.0" + const val ROOM = "2.7.0" + const val HAMCREST = "3.0" + const val NAVIGATION = "2.9.5" + const val MEDIA3 = "1.8.0" + const val DATASTORE = "1.1.7" + const val LOTTIE = "6.6.6" + const val ENCRYPTED_SHARED_PREFERENCES = "1.1.0" const val JAVA_JWT = "4.5.0" const val GLANCE = "1.1.1" + const val LIVEDATA = "1.9.0" } object Libs { @@ -63,27 +65,27 @@ object Libs { const val ANDROIDX_APPCOMPAT = "androidx.appcompat:appcompat:1.7.0" const val ANDROIDX_BROWSER = "androidx.browser:browser:1.8.0" const val ANDROIDX_CARDVIEW = "androidx.cardview:cardview:1.0.0" - const val ANDROIDX_CONSTRAINT_LAYOUT = "androidx.constraintlayout:constraintlayout:2.1.4" + const val ANDROIDX_CONSTRAINT_LAYOUT = "androidx.constraintlayout:constraintlayout:2.2.0" const val ANDROIDX_EXIF = "androidx.exifinterface:exifinterface:1.3.7" const val ANDROIDX_FRAGMENT = "androidx.fragment:fragment:${Versions.FRAGMENT}" const val ANDROIDX_FRAGMENT_KTX = "androidx.fragment:fragment-ktx:${Versions.FRAGMENT}" const val ANDROIDX_PALETTE = "androidx.palette:palette:1.0.0" const val ANDROIDX_PERCENT = "androidx.percentlayout:percentlayout:1.0.0" - const val ANDROIDX_RECYCLERVIEW = "androidx.recyclerview:recyclerview:1.3.2" + const val ANDROIDX_RECYCLERVIEW = "androidx.recyclerview:recyclerview:1.4.0" const val ANDROIDX_VECTOR = "androidx.vectordrawable:vectordrawable:1.2.0" const val ANDROIDX_SWIPE_REFRESH_LAYOUT = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" const val ANDROIDX_CORE_TESTING = "androidx.arch.core:core-testing:2.2.0" const val ANDROIDX_WORK_MANAGER = "androidx.work:work-runtime:${Versions.WORK_MANAGER}" const val ANDROIDX_WORK_MANAGER_KTX = "androidx.work:work-runtime-ktx:${Versions.WORK_MANAGER}" - const val ANDROIDX_WEBKIT = "androidx.webkit:webkit:1.9.0" - const val ANDROIDX_DATABINDING_COMPILER = "androidx.databinding:databinding-compiler:${Versions.ANDROID_GRADLE_TOOLS}" // This is bundled with the gradle plugin so we use the same version - const val ANDROIDX_COMPOSE_ACTIVITY = "androidx.activity:activity-compose:1.9.0" + const val ANDROIDX_WORK_TEST = "androidx.work:work-testing:${Versions.WORK_MANAGER}" + const val ANDROIDX_WEBKIT = "androidx.webkit:webkit:1.12.0" + const val ANDROIDX_COMPOSE_ACTIVITY = "androidx.activity:activity-compose:1.10.0" const val DATASTORE = "androidx.datastore:datastore-preferences:${Versions.DATASTORE}" const val ENCRYPTED_SHARED_PREFERENCES = "androidx.security:security-crypto:${Versions.ENCRYPTED_SHARED_PREFERENCES}" const val JAVA_JWT = "com.auth0:java-jwt:${Versions.JAVA_JWT}" /* Firebase */ - const val FIREBASE_BOM = "com.google.firebase:firebase-bom:33.4.0" + const val FIREBASE_BOM = "com.google.firebase:firebase-bom:34.3.0" const val FIREBASE_CRASHLYTICS = "com.google.firebase:firebase-crashlytics" const val FIREBASE_MESSAGING = "com.google.firebase:firebase-messaging" const val FIREBASE_CONFIG = "com.google.firebase:firebase-config" @@ -92,7 +94,7 @@ object Libs { /* Google Dependencies */ const val PLAY_IN_APP_UPDATES = "com.google.android.play:app-update:2.1.0" const val FLEXBOX_LAYOUT = "com.google.android.flexbox:flexbox:3.0.0" - const val MATERIAL_DESIGN = "com.google.android.material:material:1.12.0" + const val MATERIAL_DESIGN = "com.google.android.material:material:1.13.0" /* Mobius */ const val MOBIUS_CORE = "com.spotify.mobius:mobius-core:${Versions.MOBIUS}" @@ -129,9 +131,8 @@ object Libs { const val LIFECYCLE_COMPILER = "androidx.lifecycle:lifecycle-compiler:${Versions.LIFECYCLE}" const val COMPOSE_VIEW_MODEL = "androidx.lifecycle:lifecycle-viewmodel-compose:${Versions.LIFECYCLE}" const val COMPOSE_NAVIGATION = "androidx.navigation:navigation-compose:2.8.9" - /* Media and content handling */ - const val PSPDFKIT = "com.pspdfkit:pspdfkit:${Versions.PSPDFKIT}" + const val NUTRIENT = "io.nutrient:nutrient:${Versions.NUTRIENT}" const val MEDIA3 = "androidx.media3:media3-exoplayer:${Versions.MEDIA3}" const val MEDIA3_UI = "androidx.media3:media3-ui:${Versions.MEDIA3}" const val MEDIA3_HLS = "androidx.media3:media3-exoplayer-hls:${Versions.MEDIA3}" @@ -169,7 +170,7 @@ object Libs { const val APACHE_COMMONS_TEXT = "org.apache.commons:commons-text:1.12.0" const val CAMERA_VIEW = "com.otaliastudios:cameraview:2.7.2" - const val PENDO = "sdk.pendo.io:pendoIO:3.6+" + const val PENDO = "sdk.pendo.io:pendoIO:3.7.+" const val ROOM = "androidx.room:room-runtime:${Versions.ROOM}" const val ROOM_COMPILER = "androidx.room:room-compiler:${Versions.ROOM}" @@ -181,7 +182,7 @@ object Libs { const val RRULE = "org.scala-saddle:google-rfc-2445:20110304" // Compose - const val COMPOSE_BOM = "androidx.compose:compose-bom:2024.09.02" + const val COMPOSE_BOM = "androidx.compose:compose-bom:2025.09.01" const val COMPOSE_MATERIAL = "androidx.compose.material:material" const val COMPOSE_MATERIAL_ICONS = "androidx.compose.material:material-icons-core" const val COMPOSE_PREVIEW = "androidx.compose.ui:ui-tooling-preview" @@ -189,11 +190,11 @@ object Libs { const val COMPOSE_UI = "androidx.compose.ui:ui-android" const val COMPOSE_UI_TEST = "androidx.compose.ui:ui-test-junit4" const val COMPOSE_UI_TEST_MANIFEST = "androidx.compose.ui:ui-test-manifest" - const val COMPOSE_MATERIAL_3 = "androidx.compose.material3:material3:1.4.0-alpha12" + const val COMPOSE_MATERIAL_3 = "androidx.compose.material3:material3:1.4.0" const val COMPOSE_ADAPTIVE = "androidx.compose.material3.adaptive:adaptive" const val COMPOSE_MATERIAL3_WINDOW_SIZE = "androidx.compose.material3:material3-window-size-class" - const val COMPOSE_NAVIGATION_HILT = "androidx.hilt:hilt-navigation-compose:1.2.0" - const val COMPOSE_FRAGMENT = "androidx.fragment:fragment-compose:1.8.6" + const val COMPOSE_NAVIGATION_HILT = "androidx.hilt:hilt-navigation-compose:1.3.0" + const val COMPOSE_FRAGMENT = "androidx.fragment:fragment-compose:1.8.9" // Glance const val GLANCE = "androidx.glance:glance:${Versions.GLANCE}" @@ -218,4 +219,5 @@ object Plugins { const val GOOGLE_SERVICES = "com.google.gms:google-services:${Versions.GOOGLE_SERVICES}" const val JACOCO_ANDROID = "com.dicedmelon.gradle:jacoco-android:${Versions.JACOCO_ANDROID}" const val HILT = "com.google.dagger:hilt-android-gradle-plugin:${Versions.HILT}" + const val KSP = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:${Versions.KSP}" } diff --git a/apps/gradle.properties b/apps/gradle.properties index 61364d13a3..01ce297dfc 100644 --- a/apps/gradle.properties +++ b/apps/gradle.properties @@ -3,4 +3,4 @@ android.enableJetifier=true android.nonFinalResIds=false android.nonTransitiveRClass=false android.useAndroidX=true -org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 \ No newline at end of file +org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 \ No newline at end of file diff --git a/apps/parent/build.gradle b/apps/parent/build.gradle index 047bcbd08e..acfadaac45 100644 --- a/apps/parent/build.gradle +++ b/apps/parent/build.gradle @@ -19,7 +19,8 @@ plugins { id 'com.android.application' id 'com.google.gms.google-services' id 'org.jetbrains.kotlin.android' - id 'kotlin-kapt' + id 'kotlin-kapt' // Keep kapt for Data Binding + id 'com.google.devtools.ksp' id 'com.google.firebase.crashlytics' id 'dagger.hilt.android.plugin' id 'org.jetbrains.kotlin.plugin.compose' @@ -40,8 +41,8 @@ android { applicationId "com.instructure.parentapp" minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode 62 - versionName "4.6.0" + versionCode 63 + versionName "4.7.0" buildConfigField "boolean", "IS_TESTING", "false" testInstrumentationRunner 'com.instructure.parentapp.ui.espresso.ParentHiltTestRunner' @@ -51,15 +52,23 @@ android { PrivateData.merge(project, "parent") } - packagingOptions { - exclude 'META-INF/maven/com.google.guava/guava/pom.xml' - exclude 'META-INF/maven/com.google.guava/guava/pom.properties' - exclude 'META-INF/DEPENDENCIES' - exclude 'META-INF/LICENSE' - exclude 'META-INF/LICENSE.txt' - exclude 'META-INF/NOTICE' - exclude 'META-INF/rxjava.properties' - exclude 'LICENSE.txt' + packaging { + resources { + pickFirsts += [ + 'META-INF/INDEX.LIST', + 'META-INF/io.netty.versions.properties' + ] + excludes += [ + 'META-INF/DEPENDENCIES', + 'META-INF/LICENSE', + 'META-INF/LICENSE.txt', + 'META-INF/NOTICE', + 'META-INF/NOTICE.txt', + 'META-INF/maven/com.google.guava/guava/pom.properties', + 'META-INF/maven/com.google.guava/guava/pom.xml', + 'META-INF/rxjava.properties' + ] + } } @@ -194,14 +203,14 @@ dependencies { /* DI */ implementation Libs.HILT - kapt Libs.HILT_COMPILER + ksp Libs.HILT_COMPILER implementation Libs.HILT_ANDROIDX_WORK - kapt Libs.HILT_ANDROIDX_COMPILER + ksp Libs.HILT_ANDROIDX_COMPILER androidTestImplementation Libs.HILT_TESTING /* ROOM */ implementation Libs.ROOM - kapt Libs.ROOM_COMPILER + ksp Libs.ROOM_COMPILER implementation Libs.ROOM_COROUTINES /* Navigation */ diff --git a/apps/parent/flank.yml b/apps/parent/flank.yml index bf30186866..8680905867 100644 --- a/apps/parent/flank.yml +++ b/apps/parent/flank.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: Pixel2.arm version: 29 diff --git a/apps/parent/flank_coverage.yml b/apps/parent/flank_coverage.yml index 07643bce00..8f8f689d5c 100644 --- a/apps/parent/flank_coverage.yml +++ b/apps/parent/flank_coverage.yml @@ -19,7 +19,7 @@ gcloud: directories-to-pull: - /sdcard/ test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.OfflineE2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubCoverage + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.OfflineE2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubCoverage device: - model: Pixel2.arm version: 29 diff --git a/apps/parent/flank_e2e.yml b/apps/parent/flank_e2e.yml index a24358fd42..d82786e4a1 100644 --- a/apps/parent/flank_e2e.yml +++ b/apps/parent/flank_e2e.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: Pixel2.arm version: 29 diff --git a/apps/parent/flank_landscape.yml b/apps/parent/flank_landscape.yml index 2369cddb2b..128342d795 100644 --- a/apps/parent/flank_landscape.yml +++ b/apps/parent/flank_landscape.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubLandscape, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubLandscape, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: Pixel2.arm version: 29 diff --git a/apps/parent/flank_multi_api_level.yml b/apps/parent/flank_multi_api_level.yml index 4213e759ef..c91e79f4e8 100644 --- a/apps/parent/flank_multi_api_level.yml +++ b/apps/parent/flank_multi_api_level.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubMultiAPILevel, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubMultiAPILevel, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: NexusLowRes version: 27 diff --git a/apps/parent/flank_tablet.yml b/apps/parent/flank_tablet.yml index 8beaae55bc..456bf4d0c4 100644 --- a/apps/parent/flank_tablet.yml +++ b/apps/parent/flank_tablet.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubTablet, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubTablet, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: MediumTablet.arm version: 29 diff --git a/apps/parent/proguard-rules.txt b/apps/parent/proguard-rules.txt index bde42b7512..ab46a57d84 100644 --- a/apps/parent/proguard-rules.txt +++ b/apps/parent/proguard-rules.txt @@ -254,4 +254,19 @@ -dontwarn java.beans.SimpleBeanInfo -keep class androidx.navigation.** { *; } - -keep interface androidx.navigation.** { *; } \ No newline at end of file + -keep interface androidx.navigation.** { *; } + +# Netty and BlockHound integration +-dontwarn reactor.blockhound.integration.BlockHoundIntegration +-dontwarn io.netty.util.internal.Hidden$NettyBlockHoundIntegration +-keep class reactor.blockhound.integration.** { *; } +-keep class io.netty.util.internal.Hidden$NettyBlockHoundIntegration { *; } + +# Additional Netty keep rules for R8 +-dontwarn io.netty.** +-keep class io.netty.** { *; } +-keepclassmembers class io.netty.** { *; } + +# BlockHound related classes +-dontwarn reactor.blockhound.** +-keep class reactor.blockhound.** { *; } diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/HelpMenuE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/classic/HelpMenuE2ETest.kt similarity index 90% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/HelpMenuE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/classic/HelpMenuE2ETest.kt index a7ea6fe649..c81a7d5432 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/HelpMenuE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/classic/HelpMenuE2ETest.kt @@ -14,21 +14,21 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.classic import android.util.Log import androidx.test.espresso.intent.Intents -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.checkToastText import com.instructure.parentapp.R import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -41,8 +41,7 @@ class HelpMenuE2ETest : ParentComposeTest() { @E2E @Test - @TestMetaData(Priority.NICE_TO_HAVE, FeatureCategory.DASHBOARD, TestCategory.E2E) - @Stub + @TestMetaData(Priority.COMMON, FeatureCategory.LEFT_SIDE_MENU, TestCategory.E2E, SecondaryFeatureCategory.HELP_MENU) fun testHelpMenuE2E() { Log.d(PREPARATION_TAG, "Seeding data.") @@ -89,8 +88,7 @@ class HelpMenuE2ETest : ParentComposeTest() { @E2E @Test - @TestMetaData(Priority.COMMON, FeatureCategory.DASHBOARD, TestCategory.E2E) - @Stub + @TestMetaData(Priority.BUG_CASE, FeatureCategory.LEFT_SIDE_MENU, TestCategory.E2E, SecondaryFeatureCategory.HELP_MENU) fun testHelpMenuReportProblemE2E() { Log.d(PREPARATION_TAG, "Seeding data.") diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AlertsE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AlertsE2ETest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AlertsE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AlertsE2ETest.kt index 5704eeac46..862af1d795 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AlertsE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AlertsE2ETest.kt @@ -13,15 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvasapi2.models.AlertType import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.SubmissionsApi @@ -30,10 +31,9 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.espresso.ViewUtils import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -74,7 +74,7 @@ class AlertsE2ETest : ParentComposeTest() { studentAlertSettingsPage.setThreshold(AlertType.ASSIGNMENT_GRADE_HIGH, "80") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open the Alerts Page.") dashboardPage.clickAlertsBottomMenu() @@ -133,7 +133,7 @@ class AlertsE2ETest : ParentComposeTest() { Thread.sleep(5000) // Allow the grading to propagate Log.d(STEP_TAG, "Navigate back to Alerts Page and refresh it.") - ViewUtils.pressBackButton(2) + pressBackButton(2) alertsPage.refresh() Log.d(ASSERTION_TAG, "Assert that the 'Assignment Grade Below 20' alert is displayed.") @@ -180,7 +180,7 @@ class AlertsE2ETest : ParentComposeTest() { studentAlertSettingsPage.setThreshold(AlertType.ASSIGNMENT_GRADE_HIGH, "80") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open the Alerts Page.") dashboardPage.clickAlertsBottomMenu() @@ -298,7 +298,7 @@ class AlertsE2ETest : ParentComposeTest() { studentAlertSettingsPage.assertPercentageThreshold(AlertType.COURSE_GRADE_LOW, "Never") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open the Alerts Page.") dashboardPage.clickAlertsBottomMenu() @@ -377,7 +377,7 @@ class AlertsE2ETest : ParentComposeTest() { studentAlertSettingsPage.assertPercentageThreshold(AlertType.COURSE_GRADE_HIGH, "80%") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open the Alerts Page.") dashboardPage.clickAlertsBottomMenu() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AssignmentDetailsE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentDetailsE2ETest.kt similarity index 95% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AssignmentDetailsE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentDetailsE2ETest.kt index cc46985157..44392f3d72 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AssignmentDetailsE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentDetailsE2ETest.kt @@ -14,15 +14,16 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.GradingType @@ -30,11 +31,10 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.espresso.ViewUtils import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -113,7 +113,7 @@ class AssignmentDetailsE2ETest : ParentComposeTest() { assignmentDetailsPage.assertSelectedAttempt(1) Log.d(STEP_TAG, "Navigate back to the course list page of the selected student.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Select the other student, '${student2.name}', who does not have any grade (and submission) for the given assignment and select this student.") dashboardPage.openStudentSelector() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AssignmentReminderE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt similarity index 78% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AssignmentReminderE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt index 25fb89b8f3..9a62c37a2e 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/AssignmentReminderE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/AssignmentReminderE2ETest.kt @@ -14,16 +14,16 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.checkToastText import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType @@ -36,8 +36,8 @@ import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.pandautils.utils.toFormattedString import com.instructure.parentapp.R import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.util.Calendar @@ -92,71 +92,71 @@ class AssignmentReminderE2ETest: ParentComposeTest() { assignmentDetailsPage.assertPageObjects() Log.d(ASSERTION_TAG, "Assert that the reminder section is displayed as well.") - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneHour = futureDueDate.apply { add(Calendar.HOUR, -1) } Log.d(STEP_TAG, "Select '1 Hour Before'.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneHour) - reminderPage.selectTime(reminderDateOneHour) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneHour) + assignmentReminderPage.selectTime(reminderDateOneHour) Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Hour Before' again.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneHour) - reminderPage.selectTime(reminderDateOneHour) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneHour) + assignmentReminderPage.selectTime(reminderDateOneHour) Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice.") checkToastText(R.string.reminderAlreadySet, activityRule.activity) Log.d(STEP_TAG, "Remove the '1 Hour Before' reminder, confirm the deletion dialog.") - reminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) Log.d(ASSERTION_TAG, "Assert that the '1 Hour Before' reminder is not displayed any more.") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) futureDueDate.apply { add(Calendar.HOUR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneWeek = futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, -1) } Log.d(STEP_TAG, "Select '1 Week Before'.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneWeek) - reminderPage.selectTime(reminderDateOneWeek) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneWeek) + assignmentReminderPage.selectTime(reminderDateOneWeek) Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up a reminder which has already passed (for example cannot pick '1 Week Before' reminder for an assignment which ends tomorrow).") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) checkToastText(R.string.reminderInPast, activityRule.activity) futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() - val reminderDateOneDay = futureDueDate.apply { add(Calendar.DAY_OF_MONTH, -1) } + val reminderDateOneDay = futureDueDate.apply { add(Calendar.DAY_OF_MONTH, -1) }.apply { add(Calendar.HOUR, -1) } Log.d(STEP_TAG, "Select '1 Day Before'.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneDay) - reminderPage.selectTime(reminderDateOneDay) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneDay) + assignmentReminderPage.selectTime(reminderDateOneDay) Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Day Before' again.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneDay) - reminderPage.selectTime(reminderDateOneDay) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneDay) + assignmentReminderPage.selectTime(reminderDateOneDay) Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice. (Because 1 days and 24 hours is the same)") checkToastText(R.string.reminderAlreadySet, activityRule.activity) @@ -169,7 +169,7 @@ class AssignmentReminderE2ETest: ParentComposeTest() { courseDetailsPage.clickAssignment(alreadyPastAssignment.name) Log.d(ASSERTION_TAG, "Assert that the reminder section is NOT displayed, because the '${alreadyPastAssignment.name}' assignment has already passed..") - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @E2E @@ -216,63 +216,63 @@ class AssignmentReminderE2ETest: ParentComposeTest() { assignmentDetailsPage.assertPageObjects() Log.d(ASSERTION_TAG, "Assert that the reminder section is displayed as well.") - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneHour = futureDueDate.apply { add(Calendar.HOUR, -1) } Log.d(STEP_TAG, "Select '1 Hour Before'.") - reminderPage.clickBeforeReminderOption("1 Hour Before") + assignmentReminderPage.clickBeforeReminderOption("1 Hour Before") Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Hour Before' again.") - reminderPage.clickBeforeReminderOption("1 Hour Before") + assignmentReminderPage.clickBeforeReminderOption("1 Hour Before") Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice.") checkToastText(R.string.reminderAlreadySet, activityRule.activity) Log.d(STEP_TAG, "Remove the '1 Hour Before' reminder, confirm the deletion dialog.") - reminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) Log.d(ASSERTION_TAG, "Assert that the '1 Hour Before' reminder is not displayed any more.") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) futureDueDate.apply { add(Calendar.HOUR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneWeek = futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, -1) } Log.d(STEP_TAG, "Select '1 Week Before'.") - reminderPage.clickBeforeReminderOption("1 Week Before") + assignmentReminderPage.clickBeforeReminderOption("1 Week Before") Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up a reminder which has already passed (for example cannot pick '1 Week Before' reminder for an assignment which ends tomorrow).") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) composeTestRule.waitForIdle() checkToastText(R.string.reminderInPast, activityRule.activity) composeTestRule.waitForIdle() futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneDay = futureDueDate.apply { add(Calendar.DAY_OF_MONTH, -1) } Log.d(STEP_TAG, "Select '1 Day Before'.") - reminderPage.clickBeforeReminderOption("1 Day Before") + assignmentReminderPage.clickBeforeReminderOption("1 Day Before") Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Day Before' again.") - reminderPage.clickBeforeReminderOption("1 Day Before") + assignmentReminderPage.clickBeforeReminderOption("1 Day Before") Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice. (Because 1 days and 24 hours is the same)") composeTestRule.waitForIdle() @@ -287,6 +287,6 @@ class AssignmentReminderE2ETest: ParentComposeTest() { courseDetailsPage.clickAssignment(alreadyPastAssignment.name) Log.d(ASSERTION_TAG, "Assert that the reminder section is NOT displayed, because the '${alreadyPastAssignment.name}' assignment has already passed..") - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } } \ No newline at end of file diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CalendarE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CalendarE2ETest.kt similarity index 99% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CalendarE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CalendarE2ETest.kt index 393e932c37..32f2c6da14 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CalendarE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CalendarE2ETest.kt @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.api.CalendarEventApi @@ -32,8 +32,8 @@ import com.instructure.dataseeding.util.CanvasNetworkAdapter import com.instructure.espresso.getDateInCanvasCalendarFormat import com.instructure.pandautils.features.calendar.CalendarPrefs import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsFrontPageE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsFrontPageE2ETest.kt similarity index 94% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsFrontPageE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsFrontPageE2ETest.kt index d5f3df0884..9bf77bcd40 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsFrontPageE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsFrontPageE2ETest.kt @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.PagesApi @@ -31,8 +31,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsSummaryE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsSummaryE2ETest.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsSummaryE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsSummaryE2ETest.kt index 63297e7354..d9d617e467 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsSummaryE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsSummaryE2ETest.kt @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.api.AssignmentsApi @@ -35,8 +35,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.util.Date diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsSyllabusE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsSyllabusE2ETest.kt similarity index 94% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsSyllabusE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsSyllabusE2ETest.kt index c052d4683c..3bc98c4b44 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseDetailsSyllabusE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseDetailsSyllabusE2ETest.kt @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.model.SubmissionType @@ -30,8 +30,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseListE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseListE2ETest.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseListE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseListE2ETest.kt index 80972ffcda..3bcb8a413e 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CourseListE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CourseListE2ETest.kt @@ -14,15 +14,15 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.GradingType @@ -31,8 +31,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CreateAccountE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CreateAccountE2ETest.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CreateAccountE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CreateAccountE2ETest.kt index 3a2fb468fe..1868d2b426 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/CreateAccountE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/CreateAccountE2ETest.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.app.Activity import android.app.Instrumentation @@ -25,15 +25,15 @@ import androidx.compose.ui.test.performClick import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intending import androidx.test.espresso.intent.matcher.IntentMatchers -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.UserApi import com.instructure.espresso.randomString import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData +import com.instructure.parentapp.utils.extensions.seedData import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.core.AllOf import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/DashboardE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/DashboardE2ETest.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/DashboardE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/DashboardE2ETest.kt index 84914485b0..c0a61f6c0f 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/DashboardE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/DashboardE2ETest.kt @@ -14,15 +14,15 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.EnrollmentsApi import com.instructure.dataseeding.api.SubmissionsApi @@ -33,8 +33,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/GradesListE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/GradesListE2ETest.kt similarity index 97% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/GradesListE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/GradesListE2ETest.kt index e2bb9b533c..043611177a 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/GradesListE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/GradesListE2ETest.kt @@ -14,15 +14,15 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.GradingType @@ -33,8 +33,8 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/InboxE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/InboxE2ETest.kt similarity index 99% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/InboxE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/InboxE2ETest.kt index 9c125bd0c8..cb486a2e8e 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/InboxE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/InboxE2ETest.kt @@ -14,18 +14,18 @@ * along with this program. If not, see . * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.os.SystemClock.sleep import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.ReleaseExclude import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.ReleaseExclude import com.instructure.canvas.espresso.refresh import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.toApiString @@ -38,8 +38,8 @@ import com.instructure.dataseeding.model.UpdateCourse import com.instructure.dataseeding.util.CanvasNetworkAdapter import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.util.Date diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/LoginE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/LoginE2ETest.kt similarity index 91% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/LoginE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/LoginE2ETest.kt index 2ed14fe38d..09c1be3df3 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/LoginE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/LoginE2ETest.kt @@ -14,22 +14,23 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E +import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.dataseeding.api.UserApi import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.espresso.withIdlingResourceDisabled import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData +import com.instructure.parentapp.utils.extensions.seedData import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -409,4 +410,43 @@ class LoginE2ETest : ParentComposeTest() { canvasNetworkSignInPage.assertPageObjects() } + @E2E + @Test + @TestMetaData(Priority.NICE_TO_HAVE, FeatureCategory.LOGIN, TestCategory.E2E) + fun testWrongDomainE2E() { + + val wrongDomain = "invalid-domain" + val validDomain = "mobileqa.beta" + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, courses = 1, parents = 1) + val parent = data.parentsList[0] + + Log.d(STEP_TAG, "Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG, "Enter non-existent domain: $wrongDomain.instructure.com.") + loginFindSchoolPage.enterDomain(wrongDomain) + + Log.d(STEP_TAG, "Click on 'Next' button on the Toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(ASSERTION_TAG, "Assert that the error page is displayed with 'You typed: $wrongDomain.instructure.com' message and the Canvas dinosaur error image.") + wrongDomainPage.assertPageObjects() + wrongDomainPage.assertYouTypedMessageDisplayed(wrongDomain) + wrongDomainPage.assertErrorPageImageDisplayed() + + Log.d(STEP_TAG, "Navigate back to the LoginFindSchoolPage.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Enter valid domain: $validDomain.instructure.com.") + loginFindSchoolPage.enterDomain(validDomain) + + Log.d(STEP_TAG, "Click on 'Next' button on the Toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(ASSERTION_TAG, "Assert that the Login Page is open.") + loginSignInPage.assertPageObjects() + } + } \ No newline at end of file diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/ManageStudentsE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/ManageStudentsE2ETest.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/ManageStudentsE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/ManageStudentsE2ETest.kt index 98c0e27f17..af4b8e6e20 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/ManageStudentsE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/ManageStudentsE2ETest.kt @@ -14,15 +14,15 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.EnrollmentsApi import com.instructure.dataseeding.api.SubmissionsApi @@ -33,8 +33,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/SettingsE2ETest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/SettingsE2ETest.kt similarity index 61% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/SettingsE2ETest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/SettingsE2ETest.kt index cb4120f0fb..cc1562470e 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/SettingsE2ETest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/e2e/compose/SettingsE2ETest.kt @@ -14,31 +14,33 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.e2e +package com.instructure.parentapp.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.checkToastText +import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.AssignmentsApi +import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.model.SubmissionType +import com.instructure.dataseeding.model.UpdateCourse import com.instructure.dataseeding.util.CanvasNetworkAdapter import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.espresso.ViewUtils import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.pandautils.utils.AppTheme import com.instructure.parentapp.R import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.seedData -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.seedData +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -228,7 +230,7 @@ class SettingsE2ETest : ParentComposeTest() { inboxSignatureSettingsPage.assertSignatureEnabledState(true) Log.d(STEP_TAG, "Navigate back to the Dashboard.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open the Left Side Navigation Drawer menu.") dashboardPage.openLeftSideMenu() @@ -432,4 +434,250 @@ class SettingsE2ETest : ParentComposeTest() { Log.d(ASSERTION_TAG, "Assert that the previously set inbox signature text is displayed by default since we logged back with '${parent.name}' teacher.") inboxComposeMessagePage.assertBodyText("\n\n---\nPresident of AC Milan\nVice President of Ferencvaros") } + + @E2E + @Test + @TestMetaData(Priority.BUG_CASE, FeatureCategory.INBOX, TestCategory.E2E, SecondaryFeatureCategory.INBOX_SIGNATURE) + fun testInboxSignaturesFABButtonWithDifferentUsersE2E() { + + //Bug Ticket: MBL-18929 + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, parents = 2, courses = 1) + val parent = data.parentsList[0] + val parent2 = data.parentsList[1] + val teacher = data.teachersList[0] + val course = data.coursesList[0] + + Log.d(PREPARATION_TAG, "Seed assignment for '${course.name}' course.") + val testAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, submissionTypes = listOf(SubmissionType.ON_PAPER), withDescription = true, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601) + + val syllabusBody = "This is the syllabus body." + Log.d(PREPARATION_TAG, "Update '${course.name}' course to set Syllabus with some syllabus body.") + CoursesApi.updateCourse(course.id, UpdateCourse(syllabusBody = syllabusBody)) + + Log.d(STEP_TAG, "Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG, "Enter domain: '${CanvasNetworkAdapter.canvasDomain}'") + loginFindSchoolPage.enterDomain(CanvasNetworkAdapter.canvasDomain) + + Log.d(STEP_TAG, "Click on 'Next' button on the toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(STEP_TAG, "Login with user: '${parent.name}', login id: '${parent.loginId}'.") + loginSignInPage.loginAs(parent) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open the Left Side Navigation Drawer menu.") + dashboardPage.openLeftSideMenu() + + Log.d(STEP_TAG, "Navigate to Settings Page on the left-side menu.") + leftSideNavigationDrawerPage.clickSettings() + + Log.d(ASSERTION_TAG, "Assert that by default the Inbox Signature is 'Not Set'.") + settingsPage.assertSettingsItemDisplayed("Inbox Signature", "Not Set") + + Log.d(STEP_TAG, "Click on the 'Inbox Signature' settings.") + settingsPage.clickOnSettingsItem("Inbox Signature") + + Log.d(ASSERTION_TAG, "Assert that by default the 'Inbox Signature' toggle is turned off.") + inboxSignatureSettingsPage.assertSignatureEnabledState(false) + + val signatureText = "Best Regards\nCanvas Parent" + Log.d(STEP_TAG, "Turn on the 'Inbox Signature' and set the inbox signature text to: '$signatureText'. Save the changes.") + inboxSignatureSettingsPage.toggleSignatureEnabledState() + inboxSignatureSettingsPage.changeSignatureText(signatureText) + inboxSignatureSettingsPage.saveChanges() + + Log.d(ASSERTION_TAG, "Assert that the 'Inbox settings saved!' toast message is displayed.") + checkToastText(R.string.inboxSignatureSettingsUpdated, activityRule.activity) + + Log.d(STEP_TAG, "Refresh the Settings page.") + settingsPage.refresh() + + Log.d(ASSERTION_TAG, "Assert that the Inbox Signature became 'Enabled'.") + settingsPage.assertSettingsItemDisplayed("Inbox Signature", "Enabled") + + Log.d(STEP_TAG, "Navigate back to the Dashboard.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Select course: '${course.name}'.") + coursesPage.clickCourseItem(course.name) + + Log.d(STEP_TAG, "Select 'GRADES' tab.") + courseDetailsPage.selectTab("GRADES") + + Log.d(STEP_TAG, "Click on the 'Compose Message' FAB on the Grades tab.") + courseDetailsPage.clickComposeMessageFAB() + + Log.d(ASSERTION_TAG, "Assert that the inbox signature: '${signatureText}' text is displayed when composing message from Grades tab FAB.") + inboxComposeMessagePage.assertBodyText("\n\n---\nBest Regards\nCanvas Parent") + + Log.d(STEP_TAG, "Click on the 'Close' (X) button on the Compose New Message Page.") + inboxComposePage.clickOnCloseButton() + + Log.d(STEP_TAG, "Select 'SYLLABUS' tab.") + courseDetailsPage.selectTab("SYLLABUS") + + Log.d(STEP_TAG, "Click on the 'Compose Message' FAB on the Syllabus tab.") + courseDetailsPage.clickComposeMessageFAB() + + Log.d(ASSERTION_TAG, "Assert that the inbox signature: '${signatureText}' text is displayed when composing message from Syllabus tab FAB.") + inboxComposeMessagePage.assertBodyText("\n\n---\nBest Regards\nCanvas Parent") + + Log.d(STEP_TAG, "Click on the 'Close' (X) button on the Compose New Message Page.") + inboxComposePage.clickOnCloseButton() + + Log.d(STEP_TAG, "Select 'SUMMARY' tab.") + courseDetailsPage.selectTab("SUMMARY") + + Log.d(STEP_TAG, "Click on the 'Compose Message' FAB on the Summary tab.") + courseDetailsPage.clickComposeMessageFAB() + + Log.d(ASSERTION_TAG, "Assert that the inbox signature: '${signatureText}' text is displayed when composing message from Summary tab FAB.") + inboxComposeMessagePage.assertBodyText("\n\n---\nBest Regards\nCanvas Parent") + + Log.d(STEP_TAG, "Click on the 'Close' (X) button on the Compose New Message Page.") + inboxComposePage.clickOnCloseButton() + + Log.d(STEP_TAG, "Select 'GRADES' tab.") + courseDetailsPage.selectTab("GRADES") + + Log.d(STEP_TAG, "Click on assignment: '${testAssignment.name}' to open Assignment Details Page.") + courseDetailsPage.clickAssignment(testAssignment.name) + + Log.d(STEP_TAG, "Click on the 'Compose Message' FAB on Assignment Details Page.") + assignmentDetailsPage.clickComposeMessageFAB() + + Log.d(ASSERTION_TAG, "Assert that the inbox signature: '${signatureText}' text is displayed when composing message from Assignment Details FAB.") + inboxComposeMessagePage.assertBodyText("\n\n---\nBest Regards\nCanvas Parent") + + Log.d(STEP_TAG, "Click on the 'Close' (X) button on the Compose New Message Page.") + inboxComposePage.clickOnCloseButton() + + Log.d(STEP_TAG, "Navigate back to Dashboard.") + pressBackButton(2) + + Log.d(STEP_TAG, "Open the Left Side Navigation Drawer menu.") + dashboardPage.openLeftSideMenu() + + Log.d(STEP_TAG, "Click on 'Change User' button on the Left Side Navigation Drawer menu.") + leftSideNavigationDrawerPage.clickChangeUser() + + Log.d(STEP_TAG, "Click on the 'Find another school' button.") + loginLandingPage.clickFindAnotherSchoolButton() + + Log.d(STEP_TAG, "Enter domain: '${CanvasNetworkAdapter.canvasDomain}'") + loginFindSchoolPage.enterDomain(CanvasNetworkAdapter.canvasDomain) + + Log.d(STEP_TAG, "Click on 'Next' button on the toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(STEP_TAG, "Login with the other user: '${parent2.name}', login id: '${parent2.loginId}'.") + loginSignInPage.loginAs(parent2) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Select course: '${course.name}'.") + coursesPage.clickCourseItem(course.name) + + Log.d(STEP_TAG, "Select 'GRADES' tab.") + courseDetailsPage.selectTab("GRADES") + + Log.d(STEP_TAG, "Click on assignment: '${testAssignment.name}' to open Assignment Details Page.") + courseDetailsPage.clickAssignment(testAssignment.name) + + Log.d(STEP_TAG, "Click on the 'Compose Message' FAB on Assignment Details Page.") + assignmentDetailsPage.clickComposeMessageFAB() + + Log.d(ASSERTION_TAG, "Assert that the previously set inbox signature text is NOT displayed since it was set for another user.") + inboxComposeMessagePage.assertBodyText("") + + Log.d(STEP_TAG, "Click on the 'Close' (X) button on the Compose New Message Page.") + inboxComposeMessagePage.clickOnCloseButton() + + Log.d(STEP_TAG, "Navigate back to Dashboard.") + pressBackButton(2) + + Log.d(STEP_TAG, "Open the Left Side Navigation Drawer menu.") + dashboardPage.openLeftSideMenu() + + Log.d(STEP_TAG, "Navigate to Settings Page on the Left Side Navigation Drawer menu.") + leftSideNavigationDrawerPage.clickSettings() + + Log.d(ASSERTION_TAG, "Assert that by default the Inbox Signature is 'Not Set'.") + settingsPage.assertSettingsItemDisplayed("Inbox Signature", "Not Set") + + Log.d(STEP_TAG, "Click on the 'Inbox Signature' settings.") + settingsPage.clickOnSettingsItem("Inbox Signature") + + Log.d(ASSERTION_TAG, "Assert that by default the 'Inbox Signature' toggle is turned off.") + inboxSignatureSettingsPage.assertSignatureEnabledState(false) + + val secondSignatureText = "Loyal member of Instructure" + Log.d(STEP_TAG, "Turn on the 'Inbox Signature' and set the inbox signature text to: '$secondSignatureText'. Save the changes.") + inboxSignatureSettingsPage.toggleSignatureEnabledState() + inboxSignatureSettingsPage.changeSignatureText(secondSignatureText) + inboxSignatureSettingsPage.saveChanges() + + Log.d(STEP_TAG, "Refresh the Settings page.") + settingsPage.refresh() + + Log.d(ASSERTION_TAG, "Assert that the Inbox Signature became 'Enabled'.") + settingsPage.assertSettingsItemDisplayed("Inbox Signature", "Enabled") + + Log.d(STEP_TAG, "Navigate back to the Dashboard.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Select course: '${course.name}'.") + coursesPage.clickCourseItem(course.name) + + Log.d(STEP_TAG, "Select 'GRADES' tab.") + courseDetailsPage.selectTab("GRADES") + + Log.d(STEP_TAG, "Click on the 'Compose Message' FAB on the Grades tab.") + courseDetailsPage.clickComposeMessageFAB() + + Log.d(ASSERTION_TAG, "Assert that the previously set inbox signature: '$secondSignatureText' text is displayed when composing message from Grades tab FAB.") + inboxComposeMessagePage.assertBodyText("\n\n---\nLoyal member of Instructure") + + Log.d(STEP_TAG, "Click on the 'Close' (X) button on the Compose New Message Page.") + inboxComposePage.clickOnCloseButton() + + Log.d(STEP_TAG, "Select 'SYLLABUS' tab.") + courseDetailsPage.selectTab("SYLLABUS") + + Log.d(STEP_TAG, "Click on the 'Compose Message' FAB on the Syllabus tab.") + courseDetailsPage.clickComposeMessageFAB() + + Log.d(ASSERTION_TAG, "Assert that the previously set inbox signature: '$secondSignatureText' text is displayed when composing message from Syllabus tab FAB.") + inboxComposeMessagePage.assertBodyText("\n\n---\nLoyal member of Instructure") + + Log.d(STEP_TAG, "Click on the 'Close' (X) button on the Compose New Message Page.") + inboxComposePage.clickOnCloseButton() + + Log.d(STEP_TAG, "Select 'SUMMARY' tab.") + courseDetailsPage.selectTab("SUMMARY") + + Log.d(STEP_TAG, "Click on the 'Compose Message' FAB on the Summary tab.") + courseDetailsPage.clickComposeMessageFAB() + + Log.d(ASSERTION_TAG, "Assert that the previously set inbox signature: '$secondSignatureText' text is displayed when composing message from Summary tab FAB.") + inboxComposeMessagePage.assertBodyText("\n\n---\nLoyal member of Instructure") + + Log.d(STEP_TAG, "Click on the 'Close' (X) button on the Compose New Message Page.") + inboxComposePage.clickOnCloseButton() + + Log.d(STEP_TAG, "Select 'GRADES' tab.") + courseDetailsPage.selectTab("GRADES") + + Log.d(STEP_TAG, "Click on assignment: '${testAssignment.name}' to open Assignment Details Page.") + courseDetailsPage.clickAssignment(testAssignment.name) + + Log.d(STEP_TAG, "Click on the 'Compose Message' FAB on Assignment Details Page.") + assignmentDetailsPage.clickComposeMessageFAB() + + Log.d(ASSERTION_TAG, "Assert that the previously set inbox signature: '$secondSignatureText' text is displayed when composing message from Assignment details FAB.") + inboxComposeMessagePage.assertBodyText("\n\n---\nLoyal member of Instructure") + } + } \ No newline at end of file diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/TestAppManager.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/TestAppManager.kt index 5677669b65..7f637c07c3 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/TestAppManager.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/espresso/TestAppManager.kt @@ -17,6 +17,7 @@ package com.instructure.parentapp.ui.espresso +import androidx.work.DefaultWorkerFactory import androidx.work.WorkerFactory import com.instructure.pandautils.features.reminder.AlarmScheduler import com.instructure.parentapp.util.BaseAppManager @@ -26,7 +27,7 @@ open class TestAppManager : BaseAppManager() { private var workerFactory: WorkerFactory? = null override fun getWorkManagerFactory(): WorkerFactory { - return workerFactory ?: WorkerFactory.getDefaultWorkerFactory() + return workerFactory ?: DefaultWorkerFactory } override fun getScheduler(): AlarmScheduler? { diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AddStudentInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AddStudentInteractionTest.kt index edebe75061..33b43480dd 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AddStudentInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AddStudentInteractionTest.kt @@ -25,12 +25,12 @@ import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addPairingCode -import com.instructure.canvas.espresso.mockCanvas.addStudent -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addPairingCode +import com.instructure.canvas.espresso.mockcanvas.addStudent +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.hamcrest.core.AllOf diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertSettingsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertSettingsInteractionTest.kt index 3d3c353509..b85ea55dda 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertSettingsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertSettingsInteractionTest.kt @@ -19,12 +19,12 @@ import androidx.compose.ui.platform.ComposeView import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addObserverAlertThreshold -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addObserverAlertThreshold +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.AlertType import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt index 8963123059..e8412bd28d 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt @@ -18,13 +18,13 @@ import androidx.compose.ui.platform.ComposeView import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignmentsToGroups -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addObserverAlert -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignmentsToGroups +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addObserverAlert +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.AlertType @@ -32,7 +32,7 @@ import com.instructure.canvasapi2.models.AlertWorkflowState import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -156,7 +156,7 @@ class AlertsInteractionTest : ParentComposeTest() { alertsPage.clickOnAlert(alert.title) assignmentDetailsPage.assertPageObjects() - assignmentDetailsPage.assertStatusSubmitted() + assignmentDetailsPage.assertStatusLate() } @Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt index e6553daca7..1e0b667d34 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt @@ -28,24 +28,25 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.checkToastText -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addAssignmentsToGroups -import com.instructure.canvas.espresso.mockCanvas.addObserverAlert -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addAssignmentsToGroups +import com.instructure.canvas.espresso.mockcanvas.addObserverAlert +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.AlertType import com.instructure.canvasapi2.models.AlertWorkflowState import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.utils.toApiString import com.instructure.pandautils.utils.toFormattedString import com.instructure.parentapp.R import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -104,7 +105,7 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { fun testDisplayDueDate() { val data = setupData() val calendar = Calendar.getInstance().apply { set(2023, 0, 31, 23, 59, 0) } - val expectedDueDate = "January 31, 2023 11:59 PM" + val expectedDueDate = "Jan 31, 2023 11:59 PM" val course = data.courses.values.first() val assignmentWithNoDueDate = data.addAssignment(course.id, name = "Test Assignment", dueAt = calendar.time.toApiString()) @@ -113,6 +114,41 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { assignmentDetailsPage.assertDisplaysDate(expectedDueDate) } + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun testDisplayDueDates() { + val data = setupData() + var calendar = Calendar.getInstance().apply { set(2023, 0, 29, 23, 59, 0) } + val expectedReplyToTopicDueDate = "Jan 29, 2023 11:59 PM" + val replyToTopicDueDate = calendar.time.toApiString() + + calendar = Calendar.getInstance().apply { set(2023, 0, 31, 23, 59, 0) } + val expectedReplyToEntryDueDate = "Jan 31, 2023 11:59 PM" + val replyToEntryDueDate = calendar.time.toApiString() + val course = data.courses.values.first() + + val checkpoints = listOf( + Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + dueAt = replyToTopicDueDate, + pointsPossible = 10.0 + ), + Checkpoint( + name = "Reply to Entry", + tag = "reply_to_entry", + dueAt = replyToEntryDueDate, + pointsPossible = 10.0 + ) + ) + val assignmentWithNoDueDate = data.addAssignment(course.id, name = "Test Assignment", dueAt = calendar.time.toApiString(), checkpoints = checkpoints) + + gotoAssignment(data, assignmentWithNoDueDate) + + assignmentDetailsPage.assertDisplaysDate(expectedReplyToTopicDueDate, 0) + assignmentDetailsPage.assertDisplaysDate(expectedReplyToEntryDueDate, 1) + } + @Test fun testNavigating_viewAssignmentDetails() { // Test clicking on the Assignment item in the Assignment List to load the Assignment Details Page @@ -269,7 +305,39 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { }.time.toApiString()) gotoAssignment(data, assignment) - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun testReminderSectionsAreVisibleWhenThereAreNoFutureDueDates() { + val data = setupData() + val course = data.courses.values.first() + + val pastDate = Calendar.getInstance().apply { + add(Calendar.DAY_OF_MONTH, -1) + }.time.toApiString() + + val checkpoints = listOf( + Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + dueAt = pastDate, + pointsPossible = 10.0 + ), + Checkpoint( + name = "Reply to Entry", + tag = "reply_to_entry", + dueAt = pastDate, + pointsPossible = 10.0 + ) + ) + val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = pastDate, checkpoints = checkpoints) + + gotoAssignment(data, assignment) + + assignmentDetailsPage.assertReminderViewDisplayed(0) + assignmentDetailsPage.assertReminderViewDisplayed(1) } @Test @@ -280,7 +348,7 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { val assignment = data.addAssignment(course.id, name = "Test Assignment") gotoAssignment(data, assignment) - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @Test @@ -293,7 +361,7 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { }.time.toApiString()) gotoAssignment(data, assignment) - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @Test @@ -309,12 +377,12 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { }.time.toApiString()) gotoAssignment(data, assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) - reminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) } @Test @@ -330,17 +398,17 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { }.time.toApiString()) gotoAssignment(data, assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) - reminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) - reminderPage.removeReminderWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.removeReminderWithText(reminderCalendar.time.toFormattedString()) - reminderPage.assertReminderNotDisplayedWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderCalendar.time.toFormattedString()) } @Test @@ -356,10 +424,10 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { }.time.toApiString()) gotoAssignment(data, assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) checkToastText(R.string.reminderInPast, activityRule.activity) } @@ -377,15 +445,15 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { }.time.toApiString()) gotoAssignment(data, assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) checkToastText(R.string.reminderAlreadySet, activityRule.activity) } @@ -423,6 +491,36 @@ class AssignmentDetailsInteractionTest : ParentComposeTest() { onWebView().check(webMatches(getCurrentUrl(), containsString(expectedUrl))) } + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun testDiscussionCheckpointsDisplayed() { + val data = setupData(false) + val course = data.courses.values.first() + + val checkpoint1 = Checkpoint( + tag = "reply_to_topic", + pointsPossible = 5.0, + dueAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 1) }.time.toApiString() + ) + val checkpoint2 = Checkpoint( + tag = "reply_to_entry", + pointsPossible = 5.0, + dueAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 2) }.time.toApiString() + ) + + val assignment = data.addAssignment( + courseId = course.id, + name = "Discussion Checkpoint Assignment", + checkpoints = listOf(checkpoint1, checkpoint2), + submissionTypeList = listOf(Assignment.SubmissionType.DISCUSSION_TOPIC) + ) + + gotoAssignment(data, assignment) + + assignmentDetailsPage.assertCheckpointDisplayed(0, "Reply to topic", "-/5") + assignmentDetailsPage.assertCheckpointDisplayed(1, "Additional replies (0)", "-/5") + } + private fun setupData(restrictQuantitativeData: Boolean = false): MockCanvas { val data = MockCanvas.init( parentCount = 1, diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CourseDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CourseDetailsInteractionTest.kt index e284cd6181..488e38a3f3 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CourseDetailsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CourseDetailsInteractionTest.kt @@ -17,13 +17,13 @@ package com.instructure.parentapp.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.Tab import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CoursesInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CoursesInteractionTest.kt index 186329b446..6c4492e6f1 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CoursesInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CoursesInteractionTest.kt @@ -17,13 +17,13 @@ package com.instructure.parentapp.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCourseWithEnrollment -import com.instructure.canvas.espresso.mockCanvas.addEnrollment -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCourseWithEnrollment +import com.instructure.canvas.espresso.mockcanvas.addEnrollment +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Enrollment import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CreateAccountInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CreateAccountInteractionTest.kt index 14e1b2528e..cb0ba0790d 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CreateAccountInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/CreateAccountInteractionTest.kt @@ -26,9 +26,9 @@ import androidx.compose.ui.test.performClick import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intending import androidx.test.espresso.intent.matcher.IntentMatchers -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addPairingCode -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addPairingCode +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.parentapp.utils.ParentComposeTest import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.core.AllOf diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt index b7cbab1033..d1e19afd22 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt @@ -25,13 +25,13 @@ import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvas.espresso.waitForMatcherWithSleeps import com.instructure.canvasapi2.utils.Pronouns import com.instructure.loginapi.login.R import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ManageStudentsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ManageStudentsInteractionTest.kt index b5f82a859e..3250a233d4 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ManageStudentsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ManageStudentsInteractionTest.kt @@ -24,10 +24,10 @@ import androidx.compose.ui.test.onNodeWithText import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.junit.Test diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt index 69342cf8f5..2da7ba5fae 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt @@ -23,17 +23,17 @@ import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.matcher.ViewMatchers -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCourse -import com.instructure.canvas.espresso.mockCanvas.addEnrollment -import com.instructure.canvas.espresso.mockCanvas.addUser -import com.instructure.canvas.espresso.mockCanvas.init -import com.instructure.canvas.espresso.mockCanvas.updateUserEnrollments +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCourse +import com.instructure.canvas.espresso.mockcanvas.addEnrollment +import com.instructure.canvas.espresso.mockcanvas.addUser +import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvas.espresso.mockcanvas.updateUserEnrollments import com.instructure.canvas.espresso.waitForMatcherWithSleeps import com.instructure.canvasapi2.models.Enrollment import com.instructure.loginapi.login.R import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.CoreMatchers import org.junit.After diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCalendarInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCalendarInteractionTest.kt index 422e6385be..ec849a83f3 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCalendarInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCalendarInteractionTest.kt @@ -21,18 +21,18 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.CalendarInteractionTest import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.User import com.instructure.espresso.ModuleItemInteractions import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.DashboardPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -51,7 +51,7 @@ class ParentCalendarInteractionTest : CalendarInteractionTest() { override val activityRule = ParentActivityTestRule(LoginActivity::class.java) private val dashboardPage = DashboardPage() - private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) + private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(), composeTestRule) override fun goToCalendar(data: MockCanvas) { val parent = data.parents.first() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateEventInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateEventInteractionTest.kt index 954aaf1deb..c5024f54e4 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateEventInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateEventInteractionTest.kt @@ -20,14 +20,14 @@ import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.CreateUpdateEventInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.DashboardPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateToDoInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateToDoInteractionTest.kt index 030aaea55b..c4b9a8a330 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateToDoInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentCreateUpdateToDoInteractionTest.kt @@ -20,14 +20,14 @@ import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.CreateUpdateToDoInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.DashboardPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentEventDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentEventDetailsInteractionTest.kt index 4a05c97fb6..17f0da5fbc 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentEventDetailsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentEventDetailsInteractionTest.kt @@ -20,13 +20,13 @@ import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.EventDetailsInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.DashboardPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentGradesInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentGradesInteractionTest.kt index 031713a7e7..bc1fe7d0f0 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentGradesInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentGradesInteractionTest.kt @@ -18,17 +18,17 @@ package com.instructure.parentapp.ui.interaction import com.instructure.canvas.espresso.common.interaction.GradesInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignmentsToGroups -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignmentsToGroups +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.CoursesPage +import com.instructure.parentapp.ui.pages.compose.CoursesPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt index 64656a3b15..9c1b9bb7b1 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt @@ -7,19 +7,19 @@ import com.google.android.apps.common.testing.accessibility.framework.checks.Spe import com.instructure.canvas.espresso.common.interaction.InboxComposeInteractionTest import com.instructure.canvas.espresso.common.pages.InboxPage import com.instructure.canvas.espresso.common.pages.compose.InboxComposePage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addRecipientsToCourse -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addRecipientsToCourse +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -38,10 +38,10 @@ import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.type.EnrollmentType import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.DashboardPage -import com.instructure.parentapp.ui.pages.ParentInboxCoursePickerPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage +import com.instructure.parentapp.ui.pages.compose.ParentInboxCoursePickerPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxDetailsInteractionTest.kt index 7341fe4d02..7e981b4d6c 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxDetailsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxDetailsInteractionTest.kt @@ -21,18 +21,18 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.InboxDetailsInteractionTest import com.instructure.canvas.espresso.common.pages.InboxPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addConversation -import com.instructure.canvas.espresso.mockCanvas.addConversationWithMultipleMessages -import com.instructure.canvas.espresso.mockCanvas.addConversations -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addConversation +import com.instructure.canvas.espresso.mockcanvas.addConversationWithMultipleMessages +import com.instructure.canvas.espresso.mockcanvas.addConversations +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.User import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.DashboardPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxListInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxListInteractionTest.kt index 881840416d..b337e1f824 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxListInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxListInteractionTest.kt @@ -20,17 +20,17 @@ import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.InboxListInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addRecipientsToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addRecipientsToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.User import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.DashboardPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxSignatureInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxSignatureInteractionTest.kt index 65bc4e5d89..a28305365a 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxSignatureInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxSignatureInteractionTest.kt @@ -20,17 +20,17 @@ import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.InboxSignatureInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -43,10 +43,10 @@ import com.instructure.canvasapi2.managers.graphql.SubmissionDetailsManager import com.instructure.canvasapi2.managers.graphql.SubmissionGradeManager import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.DashboardPage -import com.instructure.parentapp.ui.pages.LeftSideNavigationDrawerPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage +import com.instructure.parentapp.ui.pages.classic.LeftSideNavigationDrawerPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentSettingsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentSettingsInteractionTest.kt index 986ced8b8e..98715e73ef 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentSettingsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentSettingsInteractionTest.kt @@ -20,14 +20,14 @@ import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.SettingsInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.DashboardPage -import com.instructure.parentapp.ui.pages.LeftSideNavigationDrawerPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage +import com.instructure.parentapp.ui.pages.classic.LeftSideNavigationDrawerPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentToDoDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentToDoDetailsInteractionTest.kt index b809c6deba..1fd6ab164f 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentToDoDetailsInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentToDoDetailsInteractionTest.kt @@ -21,15 +21,15 @@ import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.ToDoDetailsInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.espresso.InstructureActivityTestRule import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.DashboardPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage import com.instructure.parentapp.utils.ParentActivityTestRule -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/SummaryInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/SummaryInteractionTest.kt index c52d93ce4b..838584bdd5 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/SummaryInteractionTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/SummaryInteractionTest.kt @@ -17,11 +17,11 @@ package com.instructure.parentapp.ui.interaction import androidx.test.core.app.ApplicationProvider -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCourseCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Course @@ -31,7 +31,7 @@ import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.utils.toApiString import com.instructure.pandautils.utils.getDisplayDate import com.instructure.parentapp.utils.ParentComposeTest -import com.instructure.parentapp.utils.tokenLogin +import com.instructure.parentapp.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/DashboardPage.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/DashboardPage.kt index a8e830b5ab..75145bcd10 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/DashboardPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/FrontPagePage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/FrontPagePage.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/FrontPagePage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/FrontPagePage.kt index 51f7dff1de..10c3ec2cb0 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/FrontPagePage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/FrontPagePage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.web.assertion.WebViewAssertions diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/HelpPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/HelpPage.kt similarity index 97% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/HelpPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/HelpPage.kt index 523344e1df..5fa254575a 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/HelpPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/HelpPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.classic import android.app.Instrumentation import android.content.Intent @@ -43,7 +43,7 @@ class HelpPage : BasePage(R.id.helpDialog) { private val searchGuidesLabel by OnViewWithText(R.string.searchGuides) - private val reportProblemLabel by OnViewWithText(R.string.reportProblem) + private val reportProblemLabel by OnViewWithStringTextIgnoreCase("Report a Problem") private val submitFeatureLabel by OnViewWithStringTextIgnoreCase("Submit a Feature Idea") diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/LeftSideNavigationDrawerPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/LeftSideNavigationDrawerPage.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/LeftSideNavigationDrawerPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/LeftSideNavigationDrawerPage.kt index 315e61635f..4b337d12c8 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/LeftSideNavigationDrawerPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/classic/LeftSideNavigationDrawerPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.action.ViewActions diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AddStudentBottomPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AddStudentBottomPage.kt similarity index 97% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AddStudentBottomPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AddStudentBottomPage.kt index 4371785232..4102565424 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AddStudentBottomPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AddStudentBottomPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasAnyAncestor diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AlertsPage.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertsPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AlertsPage.kt index 09f206e617..53c2ecfed0 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertsPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AlertsPage.kt @@ -12,7 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */ package com.instructure.parentapp.ui.pages + */ package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AnnouncementDetailsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AnnouncementDetailsPage.kt similarity index 97% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AnnouncementDetailsPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AnnouncementDetailsPage.kt index e148fe2763..1a6efe19a6 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AnnouncementDetailsPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/AnnouncementDetailsPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CourseDetailsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CourseDetailsPage.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CourseDetailsPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CourseDetailsPage.kt index 9e8282981e..3df5436110 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CourseDetailsPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CourseDetailsPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.graphics.Color import androidx.compose.ui.test.assertIsDisplayed diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CoursesPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CoursesPage.kt similarity index 97% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CoursesPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CoursesPage.kt index 82859ba87e..a47dea0eab 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CoursesPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CoursesPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.graphics.Color import androidx.compose.ui.test.assertIsDisplayed @@ -31,7 +31,7 @@ import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown import com.instructure.canvasapi2.models.Course -import com.instructure.composeTest.hasSiblingWithText +import com.instructure.composetest.hasSiblingWithText import com.instructure.dataseeding.model.CourseApiModel import com.instructure.espresso.assertTextColor import com.instructure.pandares.R diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CreateAccountPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CreateAccountPage.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CreateAccountPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CreateAccountPage.kt index 0529291077..4ad1c207db 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/CreateAccountPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/CreateAccountPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.isDisplayed diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ManageStudentsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/ManageStudentsPage.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ManageStudentsPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/ManageStudentsPage.kt index 81e45aa617..95972ed009 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ManageStudentsPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/ManageStudentsPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/PairingCodePage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/PairingCodePage.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/PairingCodePage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/PairingCodePage.kt index d717c245ea..61e273915f 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/PairingCodePage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/PairingCodePage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithTag diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ParentInboxCoursePickerPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/ParentInboxCoursePickerPage.kt similarity index 95% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ParentInboxCoursePickerPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/ParentInboxCoursePickerPage.kt index 580e56bcad..4e938b98f5 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ParentInboxCoursePickerPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/ParentInboxCoursePickerPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.hasAnyChild import androidx.compose.ui.test.hasText diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/QrPairingPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/QrPairingPage.kt similarity index 95% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/QrPairingPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/QrPairingPage.kt index 884b62e381..62067478e5 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/QrPairingPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/QrPairingPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.junit4.ComposeTestRule diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/StudentAlertSettingsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/StudentAlertSettingsPage.kt similarity index 99% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/StudentAlertSettingsPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/StudentAlertSettingsPage.kt index cc632d4a88..677c7c4e7e 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/StudentAlertSettingsPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/StudentAlertSettingsPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/SummaryPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/SummaryPage.kt similarity index 97% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/SummaryPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/SummaryPage.kt index a60f645392..18cbbe3bcc 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/SummaryPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/SummaryPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasAnySibling diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/SyllabusPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/SyllabusPage.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/SyllabusPage.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/SyllabusPage.kt index 2a47f16254..c0a1bcfdc2 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/SyllabusPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/compose/SyllabusPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.pages +package com.instructure.parentapp.ui.pages.compose import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.web.assertion.WebViewAssertions diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/details/AnnouncementDetailsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/details/AnnouncementDetailsRenderTest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/details/AnnouncementDetailsScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/details/AnnouncementDetailsRenderTest.kt index 6d21d0088b..33a21f486d 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/details/AnnouncementDetailsScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/details/AnnouncementDetailsRenderTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.compose.alerts.details +package com.instructure.parentapp.ui.rendertests.alerts.details import android.graphics.Color import androidx.compose.ui.test.assertHasClickAction @@ -37,7 +37,7 @@ import java.time.Instant import java.util.Date @RunWith(AndroidJUnit4::class) -class AnnouncementDetailsScreenTest { +class AnnouncementDetailsRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsListItemTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/list/AlertsListItemRenderTest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsListItemTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/list/AlertsListItemRenderTest.kt index 6c2f3e509c..1736d40d68 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsListItemTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/list/AlertsListItemRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.compose.alerts.list +package com.instructure.parentapp.ui.rendertests.alerts.list import android.graphics.Color import androidx.compose.ui.test.assertHasClickAction @@ -24,7 +24,7 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.AlertType -import com.instructure.composeTest.hasDrawable +import com.instructure.composetest.hasDrawable import com.instructure.parentapp.R import com.instructure.parentapp.features.alerts.list.AlertsItemUiState import com.instructure.parentapp.features.alerts.list.AlertsListItem @@ -36,7 +36,7 @@ import java.util.Date import java.util.Locale @RunWith(AndroidJUnit4::class) -class AlertsListItemTest { +class AlertsListItemRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/list/AlertsRenderTest.kt similarity index 99% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/list/AlertsRenderTest.kt index bf9df1d50b..78d5f75d25 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/list/AlertsRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.compose.alerts.list +package com.instructure.parentapp.ui.rendertests.alerts.list import android.graphics.Color import androidx.compose.material.ExperimentalMaterialApi @@ -41,7 +41,7 @@ import java.util.Locale @ExperimentalMaterialApi @RunWith(AndroidJUnit4::class) -class AlertsScreenTest { +class AlertsRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/settings/AlertSettingsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/settings/AlertSettingsRenderTest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/settings/AlertSettingsScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/settings/AlertSettingsRenderTest.kt index c104183749..c54236114c 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/settings/AlertSettingsScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/alerts/settings/AlertSettingsRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.parentapp.ui.compose.alerts.settings +package com.instructure.parentapp.ui.rendertests.alerts.settings import android.graphics.Color import androidx.compose.ui.test.assertHasClickAction @@ -29,13 +29,13 @@ import com.instructure.canvasapi2.models.ThresholdWorkflowState import com.instructure.canvasapi2.models.User import com.instructure.parentapp.features.alerts.settings.AlertSettingsScreen import com.instructure.parentapp.features.alerts.settings.AlertSettingsUiState -import com.instructure.parentapp.ui.pages.StudentAlertSettingsPage +import com.instructure.parentapp.ui.pages.compose.StudentAlertSettingsPage import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class AlertSettingsScreenTest { +class AlertSettingsRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/CourseDetailsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/CourseDetailsRenderTest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/CourseDetailsScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/CourseDetailsRenderTest.kt index 0be595af88..679b0ff753 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/CourseDetailsScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/CourseDetailsRenderTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.compose.courses.details +package com.instructure.parentapp.ui.rendertests.courses.details import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed @@ -39,7 +39,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class CourseDetailsScreenTest { +class CourseDetailsRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/frontpage/FrontPageScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/frontpage/FrontPageRenderTest.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/frontpage/FrontPageScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/frontpage/FrontPageRenderTest.kt index 78b37c7c9c..d040320e22 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/frontpage/FrontPageScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/frontpage/FrontPageRenderTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.compose.courses.details.frontpage +package com.instructure.parentapp.ui.rendertests.courses.details.frontpage import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed @@ -31,7 +31,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class FrontPageScreenTest { +class FrontPageRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/summary/SummaryScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/summary/SummaryRenderTest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/summary/SummaryScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/summary/SummaryRenderTest.kt index 5715619c09..c6ef376040 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/summary/SummaryScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/details/summary/SummaryRenderTest.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.parentapp.ui.compose.courses.details.summary +package com.instructure.parentapp.ui.rendertests.courses.details.summary import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed @@ -35,7 +35,7 @@ import org.junit.runner.RunWith import java.util.Calendar @RunWith(AndroidJUnit4::class) -class SummaryScreenTest { +class SummaryRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/list/CoursesScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/list/CoursesRenderTest.kt similarity index 97% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/list/CoursesScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/list/CoursesRenderTest.kt index e5724d8551..d207115a75 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/list/CoursesScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/courses/list/CoursesRenderTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.compose.courses.list +package com.instructure.parentapp.ui.rendertests.courses.list import androidx.compose.ui.graphics.Color import androidx.compose.ui.test.assertHasClickAction @@ -35,7 +35,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class CoursesScreenTest { +class CoursesRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/login/createaccount/CreateAccountScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/login/createaccount/CreateAccountRenderTest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/login/createaccount/CreateAccountScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/login/createaccount/CreateAccountRenderTest.kt index d7a7747423..e094decf5b 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/login/createaccount/CreateAccountScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/login/createaccount/CreateAccountRenderTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.compose.login.createaccount +package com.instructure.parentapp.ui.rendertests.login.createaccount import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasText @@ -33,7 +33,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class CreateAccountScreenTest { +class CreateAccountRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/managestudents/ManageStudentsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/managestudents/ManageStudentsRenderTest.kt similarity index 98% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/managestudents/ManageStudentsScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/managestudents/ManageStudentsRenderTest.kt index d265788cf9..48ebfa8d83 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/managestudents/ManageStudentsScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/managestudents/ManageStudentsRenderTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.compose.managestudents +package com.instructure.parentapp.ui.rendertests.managestudents import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed @@ -39,7 +39,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class ManageStudentsScreenTest { +class ManageStudentsRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/notaparent/NotAParentScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/notaparent/NotAParentRenderTest.kt similarity index 97% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/notaparent/NotAParentScreenTest.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/notaparent/NotAParentRenderTest.kt index 99e9adc6be..f7013aca17 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/notaparent/NotAParentScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/rendertests/notaparent/NotAParentRenderTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.ui.compose.notaparent +package com.instructure.parentapp.ui.rendertests.notaparent import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed @@ -36,7 +36,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class NotAParentScreenTest { +class NotAParentRenderTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentActivityTestRule.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentActivityTestRule.kt index 39aa54cdd1..89045f015e 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentActivityTestRule.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentActivityTestRule.kt @@ -23,7 +23,6 @@ import com.instructure.espresso.InstructureActivityTestRule import com.instructure.loginapi.login.util.LoginPrefs import com.instructure.loginapi.login.util.PreviousUsersUtils import com.instructure.pandautils.utils.PandaAppResetter -import com.instructure.pandautils.utils.ThemePrefs import com.instructure.parentapp.util.ParentPrefs @@ -34,8 +33,5 @@ class ParentActivityTestRule(activityClass: Class) : Instructur ParentPrefs.clearPrefs() PreviousUsersUtils.clear(context) LoginPrefs.clearPrefs() - - // We need to set this true so the theme selector won't stop our tests. - ThemePrefs.themeSelectionShown = true } } diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt index d13e612fb6..81beeda7f9 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt @@ -18,7 +18,8 @@ package com.instructure.parentapp.utils import androidx.compose.ui.test.junit4.createAndroidComposeRule -import com.instructure.canvas.espresso.common.pages.ReminderPage +import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage +import com.instructure.canvas.espresso.common.pages.AssignmentReminderPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventCreateEditPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventDetailsPage import com.instructure.canvas.espresso.common.pages.compose.CalendarFilterPage @@ -31,20 +32,21 @@ import com.instructure.canvas.espresso.common.pages.compose.InboxDetailsPage import com.instructure.canvas.espresso.common.pages.compose.InboxSignatureSettingsPage import com.instructure.canvas.espresso.common.pages.compose.RecipientPickerPage import com.instructure.canvas.espresso.common.pages.compose.SettingsPage +import com.instructure.espresso.ModuleItemInteractions import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.AddStudentBottomPage -import com.instructure.parentapp.ui.pages.AlertsPage -import com.instructure.parentapp.ui.pages.AnnouncementDetailsPage -import com.instructure.parentapp.ui.pages.CourseDetailsPage -import com.instructure.parentapp.ui.pages.CoursesPage -import com.instructure.parentapp.ui.pages.CreateAccountPage -import com.instructure.parentapp.ui.pages.ManageStudentsPage -import com.instructure.parentapp.ui.pages.PairingCodePage -import com.instructure.parentapp.ui.pages.ParentInboxCoursePickerPage -import com.instructure.parentapp.ui.pages.QrPairingPage -import com.instructure.parentapp.ui.pages.StudentAlertSettingsPage -import com.instructure.parentapp.ui.pages.SummaryPage +import com.instructure.parentapp.ui.pages.compose.AddStudentBottomPage +import com.instructure.parentapp.ui.pages.compose.AlertsPage +import com.instructure.parentapp.ui.pages.compose.AnnouncementDetailsPage +import com.instructure.parentapp.ui.pages.compose.CourseDetailsPage +import com.instructure.parentapp.ui.pages.compose.CoursesPage +import com.instructure.parentapp.ui.pages.compose.CreateAccountPage +import com.instructure.parentapp.ui.pages.compose.ManageStudentsPage import com.instructure.parentapp.ui.pages.compose.NotAParentPage +import com.instructure.parentapp.ui.pages.compose.PairingCodePage +import com.instructure.parentapp.ui.pages.compose.ParentInboxCoursePickerPage +import com.instructure.parentapp.ui.pages.compose.QrPairingPage +import com.instructure.parentapp.ui.pages.compose.StudentAlertSettingsPage +import com.instructure.parentapp.ui.pages.compose.SummaryPage import org.junit.Rule @@ -64,6 +66,7 @@ abstract class ParentComposeTest : ParentTest() { protected val notAParentPage = NotAParentPage(composeTestRule) protected val courseDetailsPage = CourseDetailsPage(composeTestRule) protected val summaryPage = SummaryPage(composeTestRule) + protected val inboxComposePage = InboxComposePage(composeTestRule) protected val announcementDetailsPage = AnnouncementDetailsPage(composeTestRule) protected val createAccountPage = CreateAccountPage(composeTestRule) protected val inboxDetailsPage = InboxDetailsPage(composeTestRule) @@ -78,8 +81,9 @@ abstract class ParentComposeTest : ParentTest() { protected val calendarToDoCreateUpdatePage = CalendarToDoCreateUpdatePage(composeTestRule) protected val calendarToDoDetailsPage = CalendarToDoDetailsPage(composeTestRule) protected val calendarFilterPage = CalendarFilterPage(composeTestRule) - protected val reminderPage = ReminderPage(composeTestRule) + protected val assignmentReminderPage = AssignmentReminderPage(composeTestRule) protected val inboxSignatureSettingsPage = InboxSignatureSettingsPage(composeTestRule) + protected val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(), composeTestRule) override fun displaysPageObjects() = Unit } diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt index 40dbfce721..aa0a57dc0d 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt @@ -19,21 +19,20 @@ package com.instructure.parentapp.utils import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvas.espresso.common.pages.AboutPage -import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage import com.instructure.canvas.espresso.common.pages.CanvasNetworkSignInPage import com.instructure.canvas.espresso.common.pages.InboxPage import com.instructure.canvas.espresso.common.pages.LegalPage import com.instructure.canvas.espresso.common.pages.LoginFindSchoolPage import com.instructure.canvas.espresso.common.pages.LoginLandingPage import com.instructure.canvas.espresso.common.pages.LoginSignInPage -import com.instructure.espresso.ModuleItemInteractions +import com.instructure.canvas.espresso.common.pages.WrongDomainPage import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity -import com.instructure.parentapp.ui.pages.DashboardPage -import com.instructure.parentapp.ui.pages.FrontPagePage -import com.instructure.parentapp.ui.pages.HelpPage -import com.instructure.parentapp.ui.pages.LeftSideNavigationDrawerPage -import com.instructure.parentapp.ui.pages.SyllabusPage +import com.instructure.parentapp.ui.pages.classic.DashboardPage +import com.instructure.parentapp.ui.pages.classic.FrontPagePage +import com.instructure.parentapp.ui.pages.classic.HelpPage +import com.instructure.parentapp.ui.pages.classic.LeftSideNavigationDrawerPage +import com.instructure.parentapp.ui.pages.compose.SyllabusPage abstract class ParentTest : CanvasTest() { @@ -46,7 +45,6 @@ abstract class ParentTest : CanvasTest() { val dashboardPage = DashboardPage() val leftSideNavigationDrawerPage = LeftSideNavigationDrawerPage() val helpPage = HelpPage() - val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) val syllabusPage = SyllabusPage() val frontPagePage = FrontPagePage() @@ -55,6 +53,7 @@ abstract class ParentTest : CanvasTest() { val canvasNetworkSignInPage = CanvasNetworkSignInPage() val loginFindSchoolPage = LoginFindSchoolPage() val loginSignInPage = LoginSignInPage() + val wrongDomainPage = WrongDomainPage() val inboxPage = InboxPage() val legalPage = LegalPage() val aboutPage = AboutPage() diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/extensions/ParentTestExtensions.kt similarity index 96% rename from apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt rename to apps/parent/src/androidTest/java/com/instructure/parentapp/utils/extensions/ParentTestExtensions.kt index 6cb71e671b..281d6beec0 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/extensions/ParentTestExtensions.kt @@ -15,13 +15,14 @@ * */ -package com.instructure.parentapp.utils +package com.instructure.parentapp.utils.extensions import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvasapi2.models.User import com.instructure.dataseeding.api.SeedApi import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.parentapp.features.login.LoginActivity +import com.instructure.parentapp.utils.ParentTest fun ParentTest.tokenLogin(user: CanvasUserApiModel) { diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt index 529133a410..50d7b1d9cb 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt @@ -26,7 +26,6 @@ import com.instructure.pandautils.dialogs.RatingDialog import com.instructure.pandautils.features.reminder.ReminderRepository import com.instructure.pandautils.room.calendar.daos.CalendarFilterDao import com.instructure.pandautils.utils.LogoutHelper -import com.instructure.pandautils.utils.ThemePrefs import com.instructure.parentapp.util.FlutterAppMigration import com.instructure.parentapp.util.ParentLogoutHelper import com.instructure.parentapp.util.ParentPrefs @@ -73,7 +72,6 @@ class ApplicationModule { @Provides fun provideFlutterAppMigration( @ApplicationContext context: Context, - themePrefs: ThemePrefs, parentPrefs: ParentPrefs, loginPrefs: LoginPrefs, previousUsersUtils: PreviousUsersUtils, @@ -85,7 +83,6 @@ class ApplicationModule { ): FlutterAppMigration { return FlutterAppMigration( context, - themePrefs, parentPrefs, loginPrefs, previousUsersUtils, diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AssignmentDetailsModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AssignmentDetailsModule.kt index a9e4557dd2..9944ea93e5 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AssignmentDetailsModule.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AssignmentDetailsModule.kt @@ -29,6 +29,7 @@ import com.instructure.pandautils.features.assignments.details.AssignmentDetails import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRouter import com.instructure.pandautils.features.assignments.details.AssignmentDetailsSubmissionHandler import com.instructure.pandautils.utils.ColorKeeper +import com.instructure.pandautils.utils.FileDownloader import com.instructure.parentapp.features.assignment.details.ParentAssignmentDetailsBehaviour import com.instructure.parentapp.features.assignment.details.ParentAssignmentDetailsColorProvider import com.instructure.parentapp.features.assignment.details.ParentAssignmentDetailsRepository @@ -50,9 +51,10 @@ class AssignmentDetailsFragmentModule { navigation: Navigation, parentPrefs: ParentPrefs, apiPrefs: ApiPrefs, - analytics: Analytics + analytics: Analytics, + fileDownloader: FileDownloader ): AssignmentDetailsRouter { - return ParentAssignmentDetailsRouter(navigation, parentPrefs, apiPrefs, analytics) + return ParentAssignmentDetailsRouter(navigation, parentPrefs, apiPrefs, analytics, fileDownloader) } @Provides diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsScreen.kt index 68b1daf8dc..66af9ed9e6 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsScreen.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsScreen.kt @@ -430,7 +430,7 @@ private fun ThresholdDialog( ) { val context = LocalContext.current var percentage by remember { mutableStateOf(threshold.orEmpty()) } - val enabled = percentage.toIntOrNull().orDefault() in (min + 1).. - val course = courses.find { it.id == enrollment.courseId } ?: return@mapNotNull null - val user = enrollment.observedUser ?: return@mapNotNull null - StudentContextItem(course, user) - } + val studentContextItems = enrollments + .filter { it.enrollmentState in listOf("active", "invited", "creation_pending") } + .mapNotNull { enrollment -> + val course = courses.find { it.id == enrollment.courseId } ?: return@mapNotNull null + val user = enrollment.observedUser ?: return@mapNotNull null + StudentContextItem(course, user) + } + .filter { !it.course.isPastEnrolment() } _uiState.update { it.copy(screenState = ScreenState.Content, studentContextItems = studentContextItems.distinct()) } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/util/FlutterAppMigration.kt b/apps/parent/src/main/java/com/instructure/parentapp/util/FlutterAppMigration.kt index adee092bea..2762e3b6ec 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/util/FlutterAppMigration.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/util/FlutterAppMigration.kt @@ -37,7 +37,6 @@ import com.instructure.pandautils.dialogs.RatingDialog import com.instructure.pandautils.features.reminder.ReminderRepository import com.instructure.pandautils.room.calendar.daos.CalendarFilterDao import com.instructure.pandautils.room.calendar.entities.CalendarFilterEntity -import com.instructure.pandautils.utils.ThemePrefs import com.instructure.pandautils.utils.fromJson import com.instructure.pandautils.utils.orDefault import dagger.hilt.android.qualifiers.ApplicationContext @@ -95,7 +94,6 @@ data class FlutterSignedInUser( class FlutterAppMigration( @ApplicationContext private val context: Context, - private val themePrefs: ThemePrefs, private val parentPrefs: ParentPrefs, private val loginPrefs: LoginPrefs, private val previousUsersUtils: PreviousUsersUtils, @@ -108,22 +106,11 @@ class FlutterAppMigration( fun migrateIfNecessary() { if (!parentPrefs.hasMigrated) { parentPrefs.hasMigrated = true - migratePrefs() migrateEncryptedSharedPrefs() migrateDatabase() } } - private fun migratePrefs() = try { - val prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) - - val isDarkMode = prefs.getBoolean(KEY_DARK_MODE, false) - - themePrefs.appTheme = if (isDarkMode) 1 else 0 - } catch (e: Exception) { - e.printStackTrace() - } - private fun migrateEncryptedSharedPrefs() = try { val masterKey = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerViewModelTest.kt index 6055f73a9b..f8a9d032b6 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerViewModelTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerViewModelTest.kt @@ -99,7 +99,10 @@ class ParentInboxCoursePickerViewModelTest { fun `loadCoursePickerItems should update uiState with data when enrollments and courses are successful`() { val courses = listOf(Course(1, "Course 1"), Course(2, "Course 2")) val users = listOf(User(1, "User 1"), User(2, "User 2")) - val enrollments = listOf(Enrollment(1, courseId = 1, observedUser = users[0]), Enrollment(2, courseId = 2, observedUser = users[1])) + val enrollments = listOf( + Enrollment(1, courseId = 1, enrollmentState = "active", observedUser = users[0]), + Enrollment(2, courseId = 2, enrollmentState = "active", observedUser = users[1]) + ) coEvery { repository.getCourses() } returns DataResult.Success(courses) coEvery { repository.getEnrollments() } returns DataResult.Success(enrollments) @@ -112,6 +115,123 @@ class ParentInboxCoursePickerViewModelTest { assertEquals(users[1], viewModel.uiState.value.studentContextItems[1].user) } + @Test + fun `loadCoursePickerItems should filter out completed and inactive enrollments`() { + val courses = listOf( + Course(1, "Active Course"), + Course(2, "Completed Course"), + Course(3, "Inactive Course") + ) + val users = listOf(User(1, "User 1"), User(2, "User 2"), User(3, "User 3")) + val enrollments = listOf( + Enrollment(1, courseId = 1, enrollmentState = "active", observedUser = users[0]), + Enrollment(2, courseId = 2, enrollmentState = "completed", observedUser = users[1]), + Enrollment(3, courseId = 3, enrollmentState = "inactive", observedUser = users[2]) + ) + coEvery { repository.getCourses() } returns DataResult.Success(courses) + coEvery { repository.getEnrollments() } returns DataResult.Success(enrollments) + + val viewModel = getViewModel() + assertEquals(ScreenState.Content, viewModel.uiState.value.screenState) + assertEquals(1, viewModel.uiState.value.studentContextItems.size) + assertEquals(courses[0], viewModel.uiState.value.studentContextItems[0].course) + assertEquals(users[0], viewModel.uiState.value.studentContextItems[0].user) + } + + @Test + fun `loadCoursePickerItems should include active, invited, and creation_pending enrollments`() { + val courses = listOf( + Course(1, "Active Course"), + Course(2, "Invited Course"), + Course(3, "Creation Pending Course") + ) + val users = listOf(User(1, "User 1"), User(2, "User 2"), User(3, "User 3")) + val enrollments = listOf( + Enrollment(1, courseId = 1, enrollmentState = "active", observedUser = users[0]), + Enrollment(2, courseId = 2, enrollmentState = "invited", observedUser = users[1]), + Enrollment(3, courseId = 3, enrollmentState = "creation_pending", observedUser = users[2]) + ) + coEvery { repository.getCourses() } returns DataResult.Success(courses) + coEvery { repository.getEnrollments() } returns DataResult.Success(enrollments) + + val viewModel = getViewModel() + assertEquals(ScreenState.Content, viewModel.uiState.value.screenState) + assertEquals(3, viewModel.uiState.value.studentContextItems.size) + assertEquals(courses[0], viewModel.uiState.value.studentContextItems[0].course) + assertEquals(users[0], viewModel.uiState.value.studentContextItems[0].user) + assertEquals(courses[1], viewModel.uiState.value.studentContextItems[1].course) + assertEquals(users[1], viewModel.uiState.value.studentContextItems[1].user) + assertEquals(courses[2], viewModel.uiState.value.studentContextItems[2].course) + assertEquals(users[2], viewModel.uiState.value.studentContextItems[2].user) + } + + @Test + fun `loadCoursePickerItems should filter out soft concluded courses with past term dates`() { + val pastTermEndDate = "2024-01-01T00:00:00Z" + val futureTermEndDate = "2026-12-31T23:59:59Z" + val courses = listOf( + Course(1, "Current Course", term = com.instructure.canvasapi2.models.Term(endAt = futureTermEndDate)), + Course(2, "Soft Concluded Course", term = com.instructure.canvasapi2.models.Term(endAt = pastTermEndDate)) + ) + val users = listOf(User(1, "User 1"), User(2, "User 2")) + val enrollments = listOf( + Enrollment(1, courseId = 1, enrollmentState = "active", observedUser = users[0]), + Enrollment(2, courseId = 2, enrollmentState = "active", observedUser = users[1]) + ) + coEvery { repository.getCourses() } returns DataResult.Success(courses) + coEvery { repository.getEnrollments() } returns DataResult.Success(enrollments) + + val viewModel = getViewModel() + assertEquals(ScreenState.Content, viewModel.uiState.value.screenState) + assertEquals(1, viewModel.uiState.value.studentContextItems.size) + assertEquals(courses[0], viewModel.uiState.value.studentContextItems[0].course) + assertEquals(users[0], viewModel.uiState.value.studentContextItems[0].user) + } + + @Test + fun `loadCoursePickerItems should filter out soft concluded courses with past course end dates`() { + val pastCourseEndDate = "2024-01-01T00:00:00Z" + val futureCourseEndDate = "2026-12-31T23:59:59Z" + val courses = listOf( + Course(1, "Current Course", endAt = futureCourseEndDate, restrictEnrollmentsToCourseDate = true), + Course(2, "Soft Concluded Course", endAt = pastCourseEndDate, restrictEnrollmentsToCourseDate = true) + ) + val users = listOf(User(1, "User 1"), User(2, "User 2")) + val enrollments = listOf( + Enrollment(1, courseId = 1, enrollmentState = "active", observedUser = users[0]), + Enrollment(2, courseId = 2, enrollmentState = "active", observedUser = users[1]) + ) + coEvery { repository.getCourses() } returns DataResult.Success(courses) + coEvery { repository.getEnrollments() } returns DataResult.Success(enrollments) + + val viewModel = getViewModel() + assertEquals(ScreenState.Content, viewModel.uiState.value.screenState) + assertEquals(1, viewModel.uiState.value.studentContextItems.size) + assertEquals(courses[0], viewModel.uiState.value.studentContextItems[0].course) + assertEquals(users[0], viewModel.uiState.value.studentContextItems[0].user) + } + + @Test + fun `loadCoursePickerItems should filter out hard concluded courses with completed workflow state`() { + val courses = listOf( + Course(1, "Active Course", workflowState = Course.WorkflowState.AVAILABLE), + Course(2, "Hard Concluded Course", workflowState = Course.WorkflowState.COMPLETED) + ) + val users = listOf(User(1, "User 1"), User(2, "User 2")) + val enrollments = listOf( + Enrollment(1, courseId = 1, enrollmentState = "active", observedUser = users[0]), + Enrollment(2, courseId = 2, enrollmentState = "active", observedUser = users[1]) + ) + coEvery { repository.getCourses() } returns DataResult.Success(courses) + coEvery { repository.getEnrollments() } returns DataResult.Success(enrollments) + + val viewModel = getViewModel() + assertEquals(ScreenState.Content, viewModel.uiState.value.screenState) + assertEquals(1, viewModel.uiState.value.studentContextItems.size) + assertEquals(courses[0], viewModel.uiState.value.studentContextItems[0].course) + assertEquals(users[0], viewModel.uiState.value.studentContextItems[0].user) + } + private fun getViewModel(): ParentInboxCoursePickerViewModel { return ParentInboxCoursePickerViewModel(context, repository, apiPrefs) } diff --git a/apps/parent/src/test/java/com/instructure/parentapp/util/FlutterAppMigrationTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/util/FlutterAppMigrationTest.kt index fb7ae99a86..1d1c4aad39 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/util/FlutterAppMigrationTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/util/FlutterAppMigrationTest.kt @@ -35,7 +35,6 @@ import com.instructure.pandautils.dialogs.RatingDialog import com.instructure.pandautils.features.reminder.ReminderRepository import com.instructure.pandautils.room.calendar.daos.CalendarFilterDao import com.instructure.pandautils.room.calendar.entities.CalendarFilterEntity -import com.instructure.pandautils.utils.ThemePrefs import com.instructure.parentapp.R import io.mockk.coVerify import io.mockk.every @@ -53,7 +52,6 @@ import java.time.ZoneId class FlutterAppMigrationTest { private val context: Context = mockk(relaxed = true) - private val themePrefs: ThemePrefs = mockk(relaxed = true) private val parentPrefs: ParentPrefs = mockk(relaxed = true) private val loginPrefs: LoginPrefs = mockk(relaxed = true) private val previousUsersUtils: PreviousUsersUtils = mockk(relaxed = true) @@ -69,7 +67,6 @@ class FlutterAppMigrationTest { private val flutterAppMigration = FlutterAppMigration( context, - themePrefs, parentPrefs, loginPrefs, previousUsersUtils, @@ -134,16 +131,6 @@ class FlutterAppMigrationTest { coVerify(exactly = 0) { parentPrefs.hasMigrated = true } } - @Test - fun `Migrate dark mode setting`() { - every { mockSharedPreferences.getBoolean("flutter.dark_mode", false) } returns true - every { context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) } returns mockSharedPreferences - - flutterAppMigration.migrateIfNecessary() - - coVerify(exactly = 1) { themePrefs.appTheme = 1 } - } - @Test fun `Migrate last user`() { every { mockSharedPreferences.all } returns mapOf( diff --git a/apps/postProcessTestRun.bash b/apps/postProcessTestRun.bash index 83d41dc037..edddc58de8 100755 --- a/apps/postProcessTestRun.bash +++ b/apps/postProcessTestRun.bash @@ -4,8 +4,8 @@ set -e # debug log # set -x -# Script post-process our test results and reports the results to Splunk. -# Only works on Bitrise, as certain secrets (like the Splunk token) will not be defined locally. +# Script post-process our test results and reports the results to Observe. +# Only works on Bitrise, as certain secrets (like the Observe token) will not be defined locally. # Capture our command line arguments if [[ $# < 2 ]] @@ -26,14 +26,18 @@ suiteName="" # Common JSON parameters for all event types commonData="\"workflow\" : \"$BITRISE_TRIGGERED_WORKFLOW_ID\", \"app\" : \"$appName\", \"branch\" : \"$BITRISE_GIT_BRANCH\"" -# A running collection of info for all passed tests. JSON object strings are just concatenated together. +# A running collection of info for all passed tests. We'll use Newline Delimited JSON (NDJSON). successReport="" successCount=0 -# Emits collected successful test data to splunk, and zeroes out the running trackers. +# Emits collected successful test data to Observe, and zeroes out the running trackers. emitSuccessfulTestData () { + # Bails if there's nothing to report + if [[ -z "$successReport" ]]; then return; fi + #echo -e "\nSuccess payload: $successReport\n" - curl -k "https://http-inputs-inst.splunkcloud.com:443/services/collector" -H "Authorization: Splunk $SPLUNK_MOBILE_TOKEN" -d "$successReport" + # Post the data as newline-delimited JSON + curl -k "https://103443579803.collect.observeinc.com/v1/http" -H "Authorization: Bearer $OBSERVE_MOBILE_TOKEN" -H "Content-Type: application/x-ndjson" --data-binary @- <<< "$successReport" successReport="" # Reset the successReport after emitting it successCount=0 } @@ -42,31 +46,32 @@ emitSuccessfulTestData () { while IFS= read -r line do # For lines, emit a deviceSummary event and remember the suiteName - # Sample line: + # Sample line: if [[ $line =~ "testsuite name" ]] then suiteName=`echo $line | cut -d " " -f 2 | cut -d = -f 2` numTests=`echo $line | cut -d " " -f 3 | cut -d = -f 2 | tr -d '"'` numFailures=`echo $line | cut -d " " -f 4 | cut -d = -f 2 | tr -d '"'` runTime=`echo $line | cut -d " " -f 7 | cut -d = -f 2 | tr -d '"'` - - payload="{\"deviceConfig\" : $suiteName, \"numTests\" : $numTests, \"numFailures\" : $numFailures, \"runTime\" : $runTime, $commonData}" + + # This is the event data + eventPayload="{\"deviceConfig\" : $suiteName, \"numTests\" : $numTests, \"numFailures\" : $numFailures, \"runTime\" : $runTime, $commonData}" + # Wrap it in the "data" object for Observe + payload="{\"data\": $eventPayload}" echo -e "\nsummary payload: $payload" - curl -k "https://http-inputs-inst.splunkcloud.com:443/services/collector" -H "Authorization: Splunk $SPLUNK_MOBILE_TOKEN" -d "{\"sourcetype\" : \"mobile-android-qa-summary\", \"event\" : $payload}" + curl -k "https://103443579803.collect.observeinc.com/v1/http" -H "Authorization: Bearer $OBSERVE_MOBILE_TOKEN" -H "Content-Type: application/json" -d "$payload" fi - # For lines, create a "test passed" payload. We won't include it in our "successReport" until we've - # verified that the test didn't fail. - # Sample line: + # For lines, store the test info. We will construct the payload at + # Sample line: if [[ $line =~ "testcase name" ]] then # Remove the '<' and '>' from around the line line=`echo $line | tr -d "<>"` # Extract various fields from the line - testName=`echo $line | cut -d " " -f 2 | cut -d = -f 2` - className=`echo $line | cut -d " " -f 3 | cut -d = -f 2` - runTime=`echo $line | cut -d " " -f 4 | cut -d = -f 2 | tr -d '"'` - payload="{\"sourcetype\" : \"mobile-android-qa-testresult\", \"event\" : {\"buildUrl\" : \"$BITRISE_BUILD_URL\", \"status\" : \"passed\", \"testName\": $testName, \"testClass\" : $className, \"deviceConfig\" : $suiteName, \"runTime\" : $runTime, $commonData}}" + currentTestName=`echo $line | cut -d " " -f 2 | cut -d = -f 2` + currentClassName=`echo $line | cut -d " " -f 3 | cut -d = -f 2` + currentRunTime=`echo $line | cut -d " " -f 4 | cut -d = -f 2 | tr -d '"'` failureEncountered=false fi @@ -76,19 +81,26 @@ do failureEncountered=true fi - # If we get to the end of a testcase and no failure has been recorded, then include the test info + # If we get to the end of a testcase and no failure has been recorded, then include the test info # in our "successReport". if [[ $line =~ "" ]] then if [[ $failureEncountered = false ]] then - successReport="$successReport $payload" + # Construct the event payload for the passed test + eventPayload="{\"buildUrl\" : \"$BITRISE_BUILD_URL\", \"status\" : \"passed\", \"testName\": $currentTestName, \"testClass\" : $currentClassName, \"deviceConfig\" : $suiteName, \"runTime\" : $currentRunTime, $commonData}" + # Wrap it in the "data" object for Observe + payload="{\"data\": $eventPayload}" + + # Append the full JSON object with a newline for NDJSON format + successReport="${successReport}${payload}"$'\n' ((successCount=successCount+1)) - # Emit successful test data to Splunk every 100 tests + + # Emit successful test data to Observe every 100 tests if [ $successCount -eq 100 ] then emitSuccessfulTestData - fi + fi fi fi done < "$reportFile" @@ -138,9 +150,13 @@ do totalMinutes=$((hours * 60 + minutes)) #echo "totalMinutes: $totalMinutes" - payload="{\"minutes\" : $totalMinutes, \"cost\" : $cost, $commonData}" + + # This is the event data + eventPayload="{\"minutes\" : $totalMinutes, \"cost\" : $cost, $commonData}" + # Wrap it in the "data" object for Observe + payload="{\"data\": $eventPayload}" + echo -e "\ncost payload: $payload" - #curl -X POST -H "Content-Type: application/json" -d "$payload" $SUMOLOGIC_ENDPOINT - curl -k "https://http-inputs-inst.splunkcloud.com:443/services/collector" -H "Authorization: Splunk $SPLUNK_MOBILE_TOKEN" -d "{\"sourcetype\" : \"mobile-android-qa-cost\", \"event\" : $payload}" + curl -k "https://103443579803.collect.observeinc.com/v1/http" -H "Authorization: Bearer $OBSERVE_MOBILE_TOKEN" -H "Content-Type: application/json" -d "$payload" fi -done < "$costFile" +done < "$costFile" \ No newline at end of file diff --git a/apps/settings.gradle b/apps/settings.gradle index 6be9862e0f..99bd1d48a7 100644 --- a/apps/settings.gradle +++ b/apps/settings.gradle @@ -7,7 +7,7 @@ pluginManagement { } } dependencies { - classpath("com.android.tools:r8:8.2.47") + classpath("com.android.tools:r8:8.13.6") } } } @@ -29,7 +29,6 @@ include ':pandautils' include ':rceditor' include ':recyclerview' include ':pandares' -include ':DocumentScanner' include ':horizon' project(':annotations').projectDir = new File(rootProject.projectDir, '/../libs/annotations') @@ -43,5 +42,4 @@ project(':pandautils').projectDir = new File(rootProject.projectDir, '/../libs/p project(':rceditor').projectDir = new File(rootProject.projectDir, '/../libs/rceditor') project(':recyclerview').projectDir = new File(rootProject.projectDir, '/../libs/recyclerview') project(':pandares').projectDir = new File(rootProject.projectDir, '/../libs/pandares') -project(':DocumentScanner').projectDir = new File(rootProject.projectDir, '/../libs/DocumentScanner') project(':horizon').projectDir = new File(rootProject.projectDir, '/../libs/horizon') diff --git a/apps/student/build.gradle b/apps/student/build.gradle index fe8ad0cd14..0f87999743 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -16,7 +16,8 @@ */ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-kapt' // Keep kapt for Data Binding +apply plugin: 'com.google.devtools.ksp' apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'org.jetbrains.kotlin.plugin.compose' @@ -61,15 +62,23 @@ android { } } - packagingOptions { - exclude 'META-INF/maven/com.google.guava/guava/pom.xml' - exclude 'META-INF/maven/com.google.guava/guava/pom.properties' - exclude 'META-INF/DEPENDENCIES' - exclude 'META-INF/LICENSE' - exclude 'META-INF/LICENSE.txt' - exclude 'META-INF/NOTICE' - exclude 'META-INF/rxjava.properties' - exclude 'LICENSE.txt' + packaging { + resources { + pickFirsts += [ + 'META-INF/INDEX.LIST', + 'META-INF/io.netty.versions.properties' + ] + excludes += [ + 'META-INF/DEPENDENCIES', + 'META-INF/LICENSE', + 'META-INF/LICENSE.txt', + 'META-INF/NOTICE', + 'META-INF/NOTICE.txt', + 'META-INF/maven/com.google.guava/guava/pom.properties', + 'META-INF/maven/com.google.guava/guava/pom.xml', + 'META-INF/rxjava.properties' + ] + } } lintOptions { @@ -227,12 +236,12 @@ dependencies { implementation project(path: ':annotations') implementation project(path: ':rceditor') implementation project(path: ':interactions') - implementation project(path: ':DocumentScanner') implementation project(path: ':horizon') /* Android Test Dependencies */ androidTestImplementation project(path: ':espresso') androidTestImplementation project(':dataseedingapi') + androidTestImplementation Libs.ANDROIDX_WORK_TEST /* Unit Test Dependencies */ testImplementation Libs.JUNIT @@ -293,15 +302,15 @@ dependencies { implementation Libs.LIVE_DATA implementation Libs.VIEW_MODE_SAVED_STATE implementation Libs.ANDROIDX_FRAGMENT_KTX - kapt Libs.LIFECYCLE_COMPILER + kapt Libs.LIFECYCLE_COMPILER // Keep kapt for lifecycle if it includes Data Binding /* DI */ implementation Libs.HILT - kapt Libs.HILT_COMPILER + ksp Libs.HILT_COMPILER androidTestImplementation Libs.HILT_TESTING - kaptAndroidTestQa Libs.HILT_TESTING_COMPILER + kspAndroidTest Libs.HILT_TESTING_COMPILER implementation Libs.HILT_ANDROIDX_WORK - kapt Libs.HILT_ANDROIDX_COMPILER + ksp Libs.HILT_ANDROIDX_COMPILER androidTestImplementation Libs.UI_AUTOMATOR @@ -313,7 +322,7 @@ dependencies { /* ROOM */ implementation Libs.ROOM - kapt Libs.ROOM_COMPILER + ksp Libs.ROOM_COMPILER implementation Libs.ROOM_COROUTINES testImplementation Libs.HAMCREST diff --git a/apps/student/flank.yml b/apps/student/flank.yml index 45f1d8bccf..19b8cb5e65 100644 --- a/apps/student/flank.yml +++ b/apps/student/flank.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: Pixel2.arm version: 29 diff --git a/apps/student/flank_coverage.yml b/apps/student/flank_coverage.yml index 3af12c3e0b..ee06d4aa34 100644 --- a/apps/student/flank_coverage.yml +++ b/apps/student/flank_coverage.yml @@ -19,7 +19,7 @@ gcloud: directories-to-pull: - /sdcard/ test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.OfflineE2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubCoverage + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.OfflineE2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubCoverage device: - model: Pixel2.arm version: 29 diff --git a/apps/student/flank_e2e.yml b/apps/student/flank_e2e.yml index 621608d4d8..063c351403 100644 --- a/apps/student/flank_e2e.yml +++ b/apps/student/flank_e2e.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: Pixel2.arm version: 29 diff --git a/apps/student/flank_e2e_coverage.yml b/apps/student/flank_e2e_coverage.yml index 55a9ea0a1b..c7da259d52 100644 --- a/apps/student/flank_e2e_coverage.yml +++ b/apps/student/flank_e2e_coverage.yml @@ -19,8 +19,8 @@ gcloud: directories-to-pull: - /sdcard/ test-targets: - - annotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.OfflineE2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubCoverage + - annotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubCoverage device: - model: Pixel2.arm version: 29 diff --git a/apps/student/flank_e2e_flaky.yml b/apps/student/flank_e2e_flaky.yml index 9a73a6fd64..f402c0b993 100644 --- a/apps/student/flank_e2e_flaky.yml +++ b/apps/student/flank_e2e_flaky.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.FlakyE2E + - annotation com.instructure.canvas.espresso.annotations.FlakyE2E device: - model: Nexus6P version: 26 diff --git a/apps/student/flank_e2e_knownbug.yml b/apps/student/flank_e2e_knownbug.yml index b1cb8ff730..5c88f700fd 100644 --- a/apps/student/flank_e2e_knownbug.yml +++ b/apps/student/flank_e2e_knownbug.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.KnownBug + - annotation com.instructure.canvas.espresso.annotations.KnownBug device: - model: Nexus6P version: 26 diff --git a/apps/student/flank_e2e_lowres.yml b/apps/student/flank_e2e_lowres.yml index d828548431..3ffe98d316 100644 --- a/apps/student/flank_e2e_lowres.yml +++ b/apps/student/flank_e2e_lowres.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: NexusLowRes version: 29 diff --git a/apps/student/flank_e2e_min.yml b/apps/student/flank_e2e_min.yml index 48817ea5d4..4f545a602f 100644 --- a/apps/student/flank_e2e_min.yml +++ b/apps/student/flank_e2e_min.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: Nexus6P version: 26 diff --git a/apps/student/flank_e2e_offline.yml b/apps/student/flank_e2e_offline.yml index 0f091632dd..1fc4ef8a8c 100644 --- a/apps/student/flank_e2e_offline.yml +++ b/apps/student/flank_e2e_offline.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.OfflineE2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - annotation com.instructure.canvas.espresso.annotations.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug device: - model: Pixel2.arm version: 29 diff --git a/apps/student/flank_landscape.yml b/apps/student/flank_landscape.yml index ebd8fc2896..4edd43e3fe 100644 --- a/apps/student/flank_landscape.yml +++ b/apps/student/flank_landscape.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubLandscape, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubLandscape, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: Pixel2.arm version: 29 diff --git a/apps/student/flank_multi_api_level.yml b/apps/student/flank_multi_api_level.yml index b0967a38ec..46d04fd3f1 100644 --- a/apps/student/flank_multi_api_level.yml +++ b/apps/student/flank_multi_api_level.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubMultiAPILevel, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubMultiAPILevel, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: NexusLowRes version: 27 diff --git a/apps/student/flank_tablet.yml b/apps/student/flank_tablet.yml index ba6afecd57..d53ad38661 100644 --- a/apps/student/flank_tablet.yml +++ b/apps/student/flank_tablet.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubTablet, com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubTablet, com.instructure.canvas.espresso.annotations.OfflineE2E device: - model: MediumTablet.arm version: 29 diff --git a/apps/student/proguard-rules.txt b/apps/student/proguard-rules.txt index aec6327015..90370642f2 100644 --- a/apps/student/proguard-rules.txt +++ b/apps/student/proguard-rules.txt @@ -269,3 +269,18 @@ -keep class androidx.navigation.** { *; } -keep interface androidx.navigation.** { *; } + +# Netty and BlockHound integration +-dontwarn reactor.blockhound.integration.BlockHoundIntegration +-dontwarn io.netty.util.internal.Hidden$NettyBlockHoundIntegration +-keep class reactor.blockhound.integration.** { *; } +-keep class io.netty.util.internal.Hidden$NettyBlockHoundIntegration { *; } + +# Additional Netty keep rules for R8 +-dontwarn io.netty.** +-keep class io.netty.** { *; } +-keepclassmembers class io.netty.** { *; } + +# BlockHound related classes +-dontwarn reactor.blockhound.** +-keep class reactor.blockhound.** { *; } diff --git a/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt b/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt index fc00cc637e..3b21a05d1d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt @@ -16,6 +16,7 @@ */ package com.instructure.student.espresso +import androidx.work.DefaultWorkerFactory import androidx.work.WorkerFactory import com.instructure.student.util.BaseAppManager @@ -24,6 +25,6 @@ open class TestAppManager : BaseAppManager() { var workerFactory: WorkerFactory? = null override fun getWorkManagerFactory(): WorkerFactory { - return workerFactory ?: WorkerFactory.getDefaultWorkerFactory() + return workerFactory ?: DefaultWorkerFactory } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt deleted file mode 100644 index 9062629753..0000000000 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (C) 2019 - present Instructure, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package com.instructure.student.ui.e2e - -import android.os.SystemClock.sleep -import android.util.Log -import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E -import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.TestCategory -import com.instructure.canvas.espresso.TestMetaData -import com.instructure.dataseeding.api.DiscussionTopicsApi -import com.instructure.espresso.ViewUtils -import com.instructure.espresso.getDateInCanvasFormat -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test - -@HiltAndroidTest -class DiscussionsE2ETest: StudentTest() { - - override fun displaysPageObjects() = Unit - - override fun enableAndConfigureAccessibilityChecks() = Unit - - @E2E - @Test - @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.E2E) - fun testDiscussionsE2E() { - - Log.d(PREPARATION_TAG, "Seeding data.") - val data = seedData(students = 1, teachers = 1, courses = 1) - val student = data.studentsList[0] - val teacher = data.teachersList[0] - val course = data.coursesList[0] - - Log.d(PREPARATION_TAG, "Seed a discussion topic for '${course.name}' course.") - val topic1 = DiscussionTopicsApi.createDiscussion(course.id, teacher.token) - - Log.d(PREPARATION_TAG, "Seed another discussion topic for '${course.name}' course.") - val topic2 = DiscussionTopicsApi.createDiscussion(course.id, teacher.token) - - Log.d(PREPARATION_TAG, "Seed an announcement for '${course.name}' course.") - val announcement = DiscussionTopicsApi.createAnnouncement(course.id, teacher.token) - - Log.d(PREPARATION_TAG, "Seed another announcement for '${course.name}' course.") - val announcement2 = DiscussionTopicsApi.createAnnouncement(course.id, teacher.token) - - Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.") - tokenLogin(student) - - Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Select course: '${course.name}'.") - dashboardPage.waitForRender() - dashboardPage.selectCourse(course) - - Log.d(ASSERTION_TAG, "Assert that the 'Discussions' and 'Announcements' Tabs are both displayed on the CourseBrowser Page.") - courseBrowserPage.assertTabDisplayed("Announcements") - courseBrowserPage.assertTabDisplayed("Discussions") - - Log.d(STEP_TAG, "Navigate to Announcements Page.") - courseBrowserPage.selectAnnouncements() - - Log.d(ASSERTION_TAG, "Assert that both '${announcement.title}' and '${announcement2.title}' announcements are displayed.") - discussionListPage.assertTopicDisplayed(announcement.title) - discussionListPage.assertTopicDisplayed(announcement2.title) - - Log.d(STEP_TAG, "Select '${announcement.title}' announcement.") - discussionListPage.selectTopic(announcement.title) - - Log.d(ASSERTION_TAG, "Assert if the Discussion Details Page is displayed.") - discussionDetailsPage.assertToolbarDiscussionTitle(announcement.title) - - Log.d(STEP_TAG, "Navigate back to the Discussion List Page.") - Espresso.pressBack() - - Log.d(STEP_TAG, "Click on the 'Search' button and search for '${announcement2.title}'. announcement.") - discussionListPage.searchable.clickOnSearchButton() - discussionListPage.searchable.typeToSearchBar(announcement2.title) - - Log.d(ASSERTION_TAG, "Refresh the page. Assert that the searching method is working well, so '${announcement.title}' won't be displayed and '${announcement2.title}' is displayed.") - discussionListPage.pullToUpdate() - discussionListPage.assertTopicDisplayed(announcement2.title) - discussionListPage.assertTopicNotDisplayed(announcement.title) - - Log.d(STEP_TAG, "Clear the search input field.") - discussionListPage.searchable.clickOnClearSearchButton() - discussionListPage.waitForDiscussionTopicToDisplay(announcement.title) - - Log.d(ASSERTION_TAG, "Assert that both announcements, '${announcement.title}' and '${announcement2.title}' has been displayed.") - discussionListPage.assertTopicDisplayed(announcement2.title) - - Log.d(STEP_TAG, "Navigate back to CourseBrowser Page.") - ViewUtils.pressBackButton(3) - - Log.d(STEP_TAG, "Navigate to Discussions Page.") - courseBrowserPage.selectDiscussions() - - Log.d(ASSERTION_TAG, "Assert that '${topic1.title}' discussion is displayed.") - discussionListPage.assertTopicDisplayed(topic1.title) - - Log.d(STEP_TAG, "Select '${topic1.title}' discussion.") - discussionListPage.selectTopic(topic1.title) - - Log.d(ASSERTION_TAG, "Assert if the details page is displayed and there is no reply for the discussion yet.") - discussionDetailsPage.assertToolbarDiscussionTitle(topic1.title) - - Log.d(STEP_TAG, "Navigate back to Discussion List Page.") - Espresso.pressBack() - - Log.d(ASSERTION_TAG, "Assert that the '${topic2.title}' discussion is displayed.") - discussionListPage.assertTopicDisplayed(topic2.title) - - Log.d(STEP_TAG, "Select '${topic2.title}' discussion.") - discussionListPage.selectTopic(topic2.title) - - Log.d(ASSERTION_TAG, "Assert if the details page is displayed and there is no reply for the discussion yet.") - discussionDetailsPage.assertToolbarDiscussionTitle(topic2.title) - - Log.d(STEP_TAG, "Navigate back to Discussion List Page.") - Espresso.pressBack() - - Log.d(STEP_TAG, "Create a new discusson then close it.") - discussionListPage.launchCreateDiscussionThenClose() - - val replyMessage = "My reply" - Log.d(PREPARATION_TAG, "Seed a discussion topic (message) entry for the '${topic1.title}' discussion with the '$replyMessage' message as a student.") - DiscussionTopicsApi.createEntryToDiscussionTopic(student.token, course.id, topic1.id, replyMessage) - sleep(2000) // Allow some time for entry creation to propagate - - Log.d(STEP_TAG, "Select '${topic1.title}' topic.") - discussionListPage.selectTopic(topic1.title) - - Log.d(ASSERTION_TAG, "Assert the the previously sent entry message, '$replyMessage')' is displayed on the details (web view) page.") - discussionDetailsPage.waitForEntryDisplayed(replyMessage) - discussionDetailsPage.assertEntryDisplayed(replyMessage) - - Log.d(STEP_TAG, "Navigate back to Discussion List Page.") - Espresso.pressBack() - - Log.d(ASSERTION_TAG, "Refresh the page. Assert that the previously sent reply has been counted, and there are no unread replies.") - discussionListPage.pullToUpdate() - discussionListPage.assertReplyCount(topic1.title, 1) - discussionListPage.assertUnreadReplyCount(topic1.title, 0) - - val currentDate = getDateInCanvasFormat() - Log.d(ASSERTION_TAG, "Assert that the due date is the current date (in the expected format).") - discussionListPage.assertDueDate(topic1.title, currentDate) - } -} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/AnnouncementsE2ETest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/AnnouncementsE2ETest.kt index ec0da2f0a3..eec078207e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/AnnouncementsE2ETest.kt @@ -14,20 +14,20 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.lang.Thread.sleep diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/CollaborationsE2ETest.kt similarity index 86% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/CollaborationsE2ETest.kt index c32d6c909e..bc426faa67 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/CollaborationsE2ETest.kt @@ -1,15 +1,15 @@ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.student.ui.pages.CollaborationsPage +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.student.ui.pages.classic.CollaborationsPage import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ConferencesE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ConferencesE2ETest.kt index ce6ea0b07e..0d4a3c27c0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ConferencesE2ETest.kt @@ -1,16 +1,16 @@ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.ConferencesApi import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DashboardE2ETest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DashboardE2ETest.kt index 875487918e..aab20d2faf 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DashboardE2ETest.kt @@ -14,20 +14,20 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.GroupsApi import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DiscussionsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DiscussionsE2ETest.kt new file mode 100644 index 0000000000..9fa6b52fb6 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/DiscussionsE2ETest.kt @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.student.ui.e2e.classic + +import android.os.SystemClock.sleep +import android.util.Log +import androidx.test.espresso.Espresso +import com.instructure.canvas.espresso.FeatureCategory +import com.instructure.canvas.espresso.Priority +import com.instructure.canvas.espresso.SecondaryFeatureCategory +import com.instructure.canvas.espresso.TestCategory +import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton +import com.instructure.dataseeding.api.DiscussionTopicsApi +import com.instructure.dataseeding.api.EnrollmentsApi +import com.instructure.dataseeding.model.EnrollmentTypes.STUDENT_ENROLLMENT +import com.instructure.espresso.getDateInCanvasFormat +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Test + +@HiltAndroidTest +class DiscussionsE2ETest: StudentComposeTest() { + + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() = Unit + + @E2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.E2E) + fun testDiscussionsE2E() { + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1) + val student = data.studentsList[0] + val teacher = data.teachersList[0] + val course = data.coursesList[0] + + Log.d(PREPARATION_TAG, "Seed a discussion topic for '${course.name}' course.") + val topic1 = DiscussionTopicsApi.createDiscussion(course.id, teacher.token) + + Log.d(PREPARATION_TAG, "Seed another discussion topic for '${course.name}' course.") + val topic2 = DiscussionTopicsApi.createDiscussion(course.id, teacher.token) + + Log.d(PREPARATION_TAG, "Seed an announcement for '${course.name}' course.") + val announcement = DiscussionTopicsApi.createAnnouncement(course.id, teacher.token) + + Log.d(PREPARATION_TAG, "Seed another announcement for '${course.name}' course.") + val announcement2 = DiscussionTopicsApi.createAnnouncement(course.id, teacher.token) + + Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.") + tokenLogin(student) + + Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Select course: '${course.name}'.") + dashboardPage.waitForRender() + dashboardPage.selectCourse(course) + + Log.d(ASSERTION_TAG, "Assert that the 'Discussions' and 'Announcements' Tabs are both displayed on the CourseBrowser Page.") + courseBrowserPage.assertTabDisplayed("Announcements") + courseBrowserPage.assertTabDisplayed("Discussions") + + Log.d(STEP_TAG, "Navigate to Announcements Page.") + courseBrowserPage.selectAnnouncements() + + Log.d(ASSERTION_TAG, "Assert that both '${announcement.title}' and '${announcement2.title}' announcements are displayed.") + discussionListPage.assertTopicDisplayed(announcement.title) + discussionListPage.assertTopicDisplayed(announcement2.title) + + Log.d(STEP_TAG, "Select '${announcement.title}' announcement.") + discussionListPage.selectTopic(announcement.title) + + Log.d(ASSERTION_TAG, "Assert if the Discussion Details Page is displayed.") + discussionDetailsPage.assertToolbarDiscussionTitle(announcement.title) + + Log.d(STEP_TAG, "Navigate back to the Discussion List Page.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Click on the 'Search' button and search for '${announcement2.title}'. announcement.") + discussionListPage.searchable.clickOnSearchButton() + discussionListPage.searchable.typeToSearchBar(announcement2.title) + + Log.d(ASSERTION_TAG, "Refresh the page. Assert that the searching method is working well, so '${announcement.title}' won't be displayed and '${announcement2.title}' is displayed.") + discussionListPage.pullToUpdate() + discussionListPage.assertTopicDisplayed(announcement2.title) + discussionListPage.assertTopicNotDisplayed(announcement.title) + + Log.d(STEP_TAG, "Clear the search input field.") + discussionListPage.searchable.clickOnClearSearchButton() + discussionListPage.waitForDiscussionTopicToDisplay(announcement.title) + + Log.d(ASSERTION_TAG, "Assert that both announcements, '${announcement.title}' and '${announcement2.title}' has been displayed.") + discussionListPage.assertTopicDisplayed(announcement2.title) + + Log.d(STEP_TAG, "Navigate back to CourseBrowser Page.") + pressBackButton(3) + + Log.d(STEP_TAG, "Navigate to Discussions Page.") + courseBrowserPage.selectDiscussions() + + Log.d(ASSERTION_TAG, "Assert that '${topic1.title}' discussion is displayed.") + discussionListPage.assertTopicDisplayed(topic1.title) + + Log.d(STEP_TAG, "Select '${topic1.title}' discussion.") + discussionListPage.selectTopic(topic1.title) + + Log.d(ASSERTION_TAG, "Assert if the details page is displayed and there is no reply for the discussion yet.") + discussionDetailsPage.assertToolbarDiscussionTitle(topic1.title) + + Log.d(STEP_TAG, "Navigate back to Discussion List Page.") + Espresso.pressBack() + + Log.d(ASSERTION_TAG, "Assert that the '${topic2.title}' discussion is displayed.") + discussionListPage.assertTopicDisplayed(topic2.title) + + Log.d(STEP_TAG, "Select '${topic2.title}' discussion.") + discussionListPage.selectTopic(topic2.title) + + Log.d(ASSERTION_TAG, "Assert if the details page is displayed and there is no reply for the discussion yet.") + discussionDetailsPage.assertToolbarDiscussionTitle(topic2.title) + + Log.d(STEP_TAG, "Navigate back to Discussion List Page.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Create a new discussion then close it.") + discussionListPage.launchCreateDiscussionThenClose() + + val replyMessage = "My reply" + Log.d(PREPARATION_TAG, "Seed a discussion topic (message) entry for the '${topic1.title}' discussion with the '$replyMessage' message as a student.") + DiscussionTopicsApi.createEntryToDiscussionTopic(student.token, course.id, topic1.id, replyMessage) + sleep(2000) // Allow some time for entry creation to propagate + + Log.d(STEP_TAG, "Select '${topic1.title}' topic.") + discussionListPage.selectTopic(topic1.title) + + Log.d(ASSERTION_TAG, "Assert the the previously sent entry message, '$replyMessage')' is displayed on the details (web view) page.") + discussionDetailsPage.waitForEntryDisplayed(replyMessage) + discussionDetailsPage.assertEntryDisplayed(replyMessage) + + Log.d(STEP_TAG, "Navigate back to Discussion List Page.") + Espresso.pressBack() + + Log.d(ASSERTION_TAG, "Refresh the page. Assert that the previously sent reply has been counted, and there are no unread replies.") + discussionListPage.pullToUpdate() + discussionListPage.assertReplyCount(topic1.title, 1) + discussionListPage.assertUnreadReplyCount(topic1.title, 0) + + val currentDate = getDateInCanvasFormat() + Log.d(ASSERTION_TAG, "Assert that the due date is the current date (in the expected format).") + discussionListPage.assertDueDate(topic1.title, currentDate) + } + + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.DISCUSSIONS, TestCategory.E2E, SecondaryFeatureCategory.DISCUSSION_CHECKPOINTS) + fun testDiscussionCheckpointsAssignmentListDetailsE2E() { + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1) + val student = data.studentsList[0] + val courseId = 3594441L + val courseName = "Kristof Deak Dedicated Test Course" + val discussionWithCheckpointsTitle = "Discussion with Checkpoints" + val discussionWithCheckpointsId = 22475794L + + Log.d(PREPARATION_TAG, "Enroll '${student.name}' student to the dedicated course ($courseName) with '$courseId' id.") + EnrollmentsApi.enrollUser(courseId, student.id, STUDENT_ENROLLMENT) + + // This will be the GraphQL way of creating a discussion with checkpoints when available. See INTEROP-9901 ticket for details. + //val disc = DiscussionTopicsApi.createDiscussionTopicWithCheckpoints(course.id, teacher.token, "Test Discussion with Checkpoints", "Test Assignment with Checkpoints") + + Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.") + tokenLogin(student) + + Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Select course: '${courseName}'.") + dashboardPage.waitForRender() + dashboardPage.selectCourse(courseName) + + Log.d(ASSERTION_TAG, "Assert that the 'Discussions' Tab is displayed on the CourseBrowser Page.") + courseBrowserPage.assertTabDisplayed("Discussions") + + Log.d(STEP_TAG, "Navigate to Assignment List Page.") + courseBrowserPage.selectAssignments() + + Log.d(ASSERTION_TAG, "Assert that the '$discussionWithCheckpointsTitle' discussion is present along with 2 date info (For the 2 checkpoints).") + assignmentListPage.assertHasAssignmentWithCheckpoints(discussionWithCheckpointsTitle, expectedGrade = "-/15") + + Log.d(STEP_TAG, "Click on the expand icon for the '$discussionWithCheckpointsTitle' discussion (to see the checkpoints' details).") + assignmentListPage.clickDiscussionCheckpointExpandCollapseIcon(discussionWithCheckpointsTitle) + + Log.d(ASSERTION_TAG, "Assert that the checkpoints' details are displayed correctly (titles, due dates, points possible, grades).") + assignmentListPage.assertDiscussionCheckpointDetails(2, "No due date", gradeReplyToTopic = "-/10", gradeAdditionalReplies = "-/5") + + Log.d(STEP_TAG, "Select '${discussionWithCheckpointsTitle}' discussion.") + assignmentListPage.clickAssignment(discussionWithCheckpointsTitle) + + Log.d(ASSERTION_TAG, "Assert that the Assignment Details Page is displayed properly with the correct toolbar title and subtitle.") + assignmentDetailsPage.assertPageObjects() + assignmentDetailsPage.assertDisplayToolbarTitle() + assignmentDetailsPage.assertDisplayToolbarSubtitle(courseName) + + Log.d(ASSERTION_TAG, "Assert that the checkpoints are displayed properly on the Assignment Details Page.") + assignmentDetailsPage.assertDiscussionCheckpointDetailsOnDetailsPage("Reply to topic due","No Due Date") + assignmentDetailsPage.assertDiscussionCheckpointDetailsOnDetailsPage("Additional replies (2) due","No Due Date") + } + + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.DISCUSSIONS, TestCategory.E2E, SecondaryFeatureCategory.DISCUSSION_CHECKPOINTS) + fun testDiscussionCheckpointsSyllabusE2E() { + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1, syllabusBody = "this is the syllabus body") // This course and syllabus will be used once the seeding will be fixed + val student = data.studentsList[0] + val courseId = 3594441L + val courseName = "Kristof Deak Dedicated Test Course" + val discussionWithCheckpointsNoDueDateTitle = "Discussion with Checkpoints" + val discussionWithCheckpointsWithDueDateTitle = "Discussion Checkpoint with due dates" + val discussionWithCheckpointsNoDueDateId = 22475794L + val discussionWithCheckpointsWithDueDatesId = 345L + + Log.d(PREPARATION_TAG, "Enroll '${student.name}' student to the dedicated course ($courseName) with '$courseId' id.") + EnrollmentsApi.enrollUser(courseId, student.id, STUDENT_ENROLLMENT) + + // This will be the GraphQL way of creating a discussion with checkpoints when available. See INTEROP-9901 ticket for details. + //val disc = DiscussionTopicsApi.createDiscussionTopicWithCheckpoints(course.id, teacher.token, "Test Discussion with Checkpoints", "Test Assignment with Checkpoints") + + Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.") + tokenLogin(student) + + Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Select course: '${courseName}'.") + dashboardPage.waitForRender() + dashboardPage.selectCourse(courseName) + + Log.d(ASSERTION_TAG, "Assert that the 'Syllabus' Tab is displayed on the CourseBrowser Page.") + courseBrowserPage.assertTabDisplayed("Syllabus") + + Log.d(STEP_TAG, "Navigate to Syllabus Page.") + courseBrowserPage.selectSyllabus() + + Log.d(ASSERTION_TAG, "Assert that all the discussions with and all their checkpoints are displayed as a separate assignment.") + syllabusPage.assertItemDisplayed("$discussionWithCheckpointsNoDueDateTitle Reply to Topic") + syllabusPage.assertItemDisplayed("$discussionWithCheckpointsNoDueDateTitle Required Replies (2)") + syllabusPage.assertItemDisplayed("$discussionWithCheckpointsWithDueDateTitle Reply to Topic") + syllabusPage.assertItemDisplayed("$discussionWithCheckpointsWithDueDateTitle Required Replies (1)") + + Log.d(STEP_TAG, "Select '$discussionWithCheckpointsNoDueDateTitle Reply to Topic' syllabus summary event.") + syllabusPage.selectSummaryEvent("$discussionWithCheckpointsNoDueDateTitle Reply to Topic") + + Log.d(ASSERTION_TAG, "Assert that the Assignment Details Page is displayed properly with the correct toolbar title and subtitle.") + assignmentDetailsPage.assertPageObjects() + assignmentDetailsPage.assertDisplayToolbarTitle() + assignmentDetailsPage.assertDisplayToolbarSubtitle(courseName) + + Log.d(ASSERTION_TAG, "Assert that there is a separate view (box) for the checkpoints with their corresponding grades.") + assignmentDetailsPage.assertCheckpointGradesView("Reply to topic", "-/100") + assignmentDetailsPage.assertCheckpointGradesView("Additional replies (1)", "-/3") + + Log.d(ASSERTION_TAG, "Assert that the checkpoints are displayed properly on the Assignment Details Page.") + assignmentDetailsPage.assertDiscussionCheckpointDetailsOnDetailsPage("Reply to topic due","Nov 12, 2025 10:59 PM") + assignmentDetailsPage.assertDiscussionCheckpointDetailsOnDetailsPage("Additional replies (1) due","Nov 19, 2025 10:59 PM") + } +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt index 04fb9f8848..03fd6afebe 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/FilesE2ETest.kt @@ -14,20 +14,20 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.os.Environment import android.util.Log -import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.test.espresso.Espresso import androidx.test.espresso.intent.Intents import androidx.test.platform.app.InstrumentationRegistry -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvasapi2.managers.DiscussionManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.weave.awaitApiResponse @@ -43,29 +43,22 @@ import com.instructure.dataseeding.util.Randomizer import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin -import com.instructure.student.ui.utils.uploadTextFile +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.extensions.uploadTextFile import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Rule import org.junit.Test import java.io.File import java.io.FileWriter @HiltAndroidTest -class FilesE2ETest: StudentTest() { +class FilesE2ETest: StudentComposeTest() { override fun displaysPageObjects() = Unit override fun enableAndConfigureAccessibilityChecks() = Unit - @get:Rule - val composeTestRule = createEmptyComposeRule() - - val assignmentListPage by lazy { AssignmentListPage(composeTestRule) } - @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.FILES, TestCategory.E2E) @@ -153,7 +146,7 @@ class FilesE2ETest: StudentTest() { fileListPage.assertItemDisplayed(submissionUploadInfo.fileName) Log.d(STEP_TAG, "Navigate back to File List Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(ASSERTION_TAG, "Assert that there is a directory called 'unfiled' is displayed.") fileListPage.assertItemDisplayed("unfiled") // Our discussion attachment goes under "unfiled" @@ -165,7 +158,7 @@ class FilesE2ETest: StudentTest() { fileListPage.assertItemDisplayed(discussionAttachmentFile.name) Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Select '${course.name}' course.") dashboardPage.selectCourse(course) @@ -190,7 +183,7 @@ class FilesE2ETest: StudentTest() { submissionDetailsPage.assertCommentAttachmentDisplayed(commentUploadInfo.fileName, student) Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(4) + pressBackButton(4) Log.d(STEP_TAG, "Navigate to 'Files' menu in user left-side menu bar.") leftSideNavigationDrawerPage.clickFilesMenu() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt index ec06b1feea..8114133b74 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/GradesE2ETest.kt @@ -1,13 +1,13 @@ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers.withText -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.QuizzesApi @@ -21,14 +21,14 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.getDateInCanvasCalendarFormat import com.instructure.student.R -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class GradesE2ETest: StudentTest() { +class GradesE2ETest: StudentComposeTest() { override fun displaysPageObjects() = Unit @@ -81,7 +81,8 @@ class GradesE2ETest: StudentTest() { courseGradesPage.assertItemDisplayed(quizMatcher) courseGradesPage.assertGradeNotDisplayed(quizMatcher) - val dueDateInCanvasFormat = getDateInCanvasCalendarFormat(1.days.fromNow.iso8601) + var dueDateInCanvasFormat = getDateInCanvasCalendarFormat(1.days.fromNow.iso8601) + dueDateInCanvasFormat = dueDateInCanvasFormat.replace(" 0", " ") Log.d(ASSERTION_TAG, "Assert that the '${assignment.name}' assignment's due date is tomorrow ('$dueDateInCanvasFormat').") courseGradesPage.assertAssignmentDueDate(assignment.name, dueDateInCanvasFormat) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/HelpMenuE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/HelpMenuE2ETest.kt similarity index 83% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/HelpMenuE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/HelpMenuE2ETest.kt index 6945cf0942..4becc7b2a6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/HelpMenuE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/HelpMenuE2ETest.kt @@ -14,21 +14,21 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.intent.Intents -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.checkToastText import com.instructure.student.R import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -42,8 +42,7 @@ class HelpMenuE2ETest : StudentTest() { @E2E @Test - @TestMetaData(Priority.NICE_TO_HAVE, FeatureCategory.DASHBOARD, TestCategory.E2E) - @Stub + @TestMetaData(Priority.COMMON, FeatureCategory.LEFT_SIDE_MENU, TestCategory.E2E, SecondaryFeatureCategory.HELP_MENU) fun testHelpMenuE2E() { Log.d(PREPARATION_TAG, "Seeding data.") @@ -63,8 +62,8 @@ class HelpMenuE2ETest : StudentTest() { Log.d(ASSERTION_TAG, "Assert that all the corresponding Help menu content are displayed.") helpPage.assertHelpMenuContent() - Log.d(STEP_TAG, "Click on 'Report a problem' menu.") - helpPage.verifyReportAProblem("Test Subject", "Test Description") + Log.d(STEP_TAG, "Click on 'Report a Problem' menu.") + helpPage.assertReportProblemDialogDetails("Test Subject", "Test Description") Log.d(ASSERTION_TAG, "Assert that it is possible to write into the input fields and the corresponding buttons are displayed as well.") helpPage.assertReportProblemDialogDisplayed() @@ -87,8 +86,7 @@ class HelpMenuE2ETest : StudentTest() { @E2E @Test - @TestMetaData(Priority.COMMON, FeatureCategory.DASHBOARD, TestCategory.E2E) - @Stub + @TestMetaData(Priority.BUG_CASE, FeatureCategory.DASHBOARD, TestCategory.E2E, SecondaryFeatureCategory.HELP_MENU) fun testHelpMenuReportProblemE2E() { Log.d(PREPARATION_TAG, "Seeding data.") @@ -108,8 +106,8 @@ class HelpMenuE2ETest : StudentTest() { Log.d(ASSERTION_TAG, "Assert that all the corresponding Help menu content are displayed.") helpPage.assertHelpMenuContent() - Log.d(STEP_TAG, "Click on 'Report a problem' menu.") - helpPage.verifyReportAProblem("Test Subject", "Test Description") + Log.d(STEP_TAG, "Click on 'Report a Problem' menu.") + helpPage.assertReportProblemDialogDetails("Test Subject", "Test Description") Log.d(ASSERTION_TAG, "Assert that it is possible to write into the input fields and the corresponding buttons are displayed as well.") helpPage.assertReportProblemDialogDisplayed() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/LoginE2ETest.kt similarity index 85% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/LoginE2ETest.kt index 1181b21559..86cf1607aa 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/LoginE2ETest.kt @@ -14,16 +14,18 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E +import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.EnrollmentsApi @@ -36,8 +38,8 @@ import com.instructure.dataseeding.model.EnrollmentTypes.TEACHER_ENROLLMENT import com.instructure.dataseeding.util.CanvasNetworkAdapter import com.instructure.espresso.withIdlingResourceDisabled import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.extensions.enterDomain +import com.instructure.student.ui.utils.extensions.seedData import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -169,7 +171,7 @@ class LoginE2ETest : StudentTest() { validateUserAndRole(student, course, "Student") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Log out with '${student.name}' student.") leftSideNavigationDrawerPage.logout() @@ -181,7 +183,7 @@ class LoginE2ETest : StudentTest() { validateUserAndRole(teacher, course, "Teacher") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Log out with '${teacher.name}' teacher.") leftSideNavigationDrawerPage.logout() @@ -193,7 +195,7 @@ class LoginE2ETest : StudentTest() { validateUserAndRole(ta, course, "TA") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Log out with '${ta.name}' teacher assistant.") leftSideNavigationDrawerPage.logout() @@ -324,7 +326,7 @@ class LoginE2ETest : StudentTest() { validateUserAndRole(student, course,"Student" ) Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Log out with '${student.name}' student.") leftSideNavigationDrawerPage.logout() @@ -343,6 +345,76 @@ class LoginE2ETest : StudentTest() { canvasNetworkSignInPage.assertPageObjects() } + @E2E + @Test + @TestMetaData(Priority.NICE_TO_HAVE, FeatureCategory.LOGIN, TestCategory.E2E) + fun testWrongDomainE2E() { + + val wrongDomain = "invalid-domain" + val validDomain = "mobileqa.beta" + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1) + val student = data.studentsList[0] + + Log.d(STEP_TAG, "Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG, "Enter non-existent domain: $wrongDomain.instructure.com.") + loginFindSchoolPage.enterDomain(wrongDomain) + + Log.d(STEP_TAG, "Click on 'Next' button on the Toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(ASSERTION_TAG, "Assert that the error page is displayed with 'You typed: $wrongDomain.instructure.com' message and the Canvas dinosaur error image.") + wrongDomainPage.assertPageObjects() + wrongDomainPage.assertYouTypedMessageDisplayed(wrongDomain) + wrongDomainPage.assertErrorPageImageDisplayed() + + Log.d(STEP_TAG, "Navigate back to the LoginFindSchoolPage.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Enter valid domain: $validDomain.instructure.com.") + loginFindSchoolPage.enterDomain(validDomain) + + Log.d(STEP_TAG, "Click on 'Next' button on the Toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(ASSERTION_TAG, "Assert that the Login Page is open.") + loginSignInPage.assertPageObjects() + } + + @Test + fun testFindSchoolPageObjects() { + + Log.d(STEP_TAG, "Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(ASSERTION_TAG, "Assert that the Find School Page has been displayed.") + loginFindSchoolPage.assertPageObjects() + } + + @Test + fun testLoginLandingPageObjects() { + + Log.d(ASSERTION_TAG, "Assert that the Login Landing Page has been displayed.") + loginLandingPage.assertPageObjects() + } + + @Test + fun testLoginSignInPageObjects() { + + Log.d(STEP_TAG, "Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG, "Enter domain: 'mobileqa.beta.instructure.com', and click on the 'Next' button on the toolbar.") + enterDomain() + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(ASSERTION_TAG, "Assert that the Login SignIn Page has been displayed.") + loginSignInPage.assertPageObjects() + } + private fun loginWithUser(user: CanvasUserApiModel, lastSchoolSaved: Boolean = false) { Thread.sleep(5100) //Need to wait > 5 seconds before each login attempt because of new 'too many attempts' login policy on web. diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ModulesE2ETest.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ModulesE2ETest.kt index c03e3b3f0a..d7e6ea4b2f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ModulesE2ETest.kt @@ -14,16 +14,16 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.dataseeding.api.ModulesApi @@ -35,14 +35,14 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class ModulesE2ETest: StudentTest() { +class ModulesE2ETest: StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/NotificationsE2ETest.kt similarity index 84% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/NotificationsE2ETest.kt index fcf9416b90..a275e5ade3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/NotificationsE2ETest.kt @@ -14,17 +14,18 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.NoMatchingViewException -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.AssignmentsApi +import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.QuizzesApi import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.GradingType @@ -35,8 +36,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.lang.Thread.sleep @@ -103,6 +104,23 @@ class NotificationsE2ETest : StudentTest() { println("API may not work properly, so not all the notifications can be seen on the screen.") } + Log.d(PREPARATION_TAG, "Seed an inbox conversation from teacher to student: '${student.name}'.") + val seededConversation = ConversationsApi.createConversation(teacher.token, listOf(student.id.toString()))[0] + + Log.d(STEP_TAG, "Navigate to Inbox page.") + dashboardPage.clickInboxTab() + + Log.d(ASSERTION_TAG, "Assert that the conversation '${seededConversation.subject}' IS displayed in the Inbox.") + inboxPage.assertConversationDisplayed(seededConversation.subject) + + Log.d(STEP_TAG, "Navigate back to Notifications page.") + dashboardPage.clickNotificationsTab() + + Log.d(ASSERTION_TAG, "Refresh the Notifications Page and verify that the conversation '${seededConversation.subject}' is NOT displayed in the Notifications page.") + refresh() + sleep(3000) //wait here to be sure that the conversation notification does not appears. + notificationPage.assertNotificationNotDisplayed(seededConversation.subject) + refresh() run submitAndGradeRepeat@{ repeat(10) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/PagesE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/PagesE2ETest.kt index 45a8f26960..eb1bd1d837 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/PagesE2ETest.kt @@ -14,21 +14,21 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.PagesApi -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PushNotificationsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/PushNotificationsE2ETest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PushNotificationsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/PushNotificationsE2ETest.kt index 860d644da9..7a903da517 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PushNotificationsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/PushNotificationsE2ETest.kt @@ -1,11 +1,11 @@ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.student.BuildConfig import com.instructure.student.ui.utils.StudentComposeTest import dagger.hilt.android.testing.HiltAndroidTest diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/QuizzesE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/QuizzesE2ETest.kt index 923201d888..14038602cd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/QuizzesE2ETest.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.matcher.ViewMatchers.withId @@ -25,21 +25,21 @@ import androidx.test.espresso.web.webdriver.DriverAtoms.findElement import androidx.test.espresso.web.webdriver.DriverAtoms.getText import androidx.test.espresso.web.webdriver.DriverAtoms.webScrollIntoView import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.containsTextCaseInsensitive +import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.QuizzesApi import com.instructure.dataseeding.model.QuizAnswer import com.instructure.dataseeding.model.QuizQuestion import com.instructure.student.R -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers.containsString import org.junit.Test @@ -157,7 +157,7 @@ class QuizzesE2ETest: StudentTest() { .check(webMatches(getText(),containsString("LATEST"))) Log.d(STEP_TAG, "Navigate back to Course Browser Page.") - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Navigate to Grades Page.") courseBrowserPage.selectGrades() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt index 3f03cf856d..cb94725be3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/ShareExtensionE2ETest.kt @@ -14,44 +14,38 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.content.Intent import android.net.Uri import android.util.Log -import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.test.espresso.Espresso import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector -import com.instructure.canvas.espresso.E2E -import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Rule import org.junit.Test @HiltAndroidTest -class ShareExtensionE2ETest: StudentTest() { +class ShareExtensionE2ETest: StudentComposeTest() { override fun displaysPageObjects() = Unit override fun enableAndConfigureAccessibilityChecks() = Unit - @get:Rule - val composeTestRule = createEmptyComposeRule() - - val assignmentListPage by lazy { AssignmentListPage(composeTestRule) } - + @Stub @E2E @Test fun shareExtensionE2ETest() { @@ -197,7 +191,7 @@ class ShareExtensionE2ETest: StudentTest() { Log.d(STEP_TAG, "Press back button to navigate back to the Dashboard Page.") assignmentDetailsPage.assertPageObjects() - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Assert that the Dashboard Page is displayed correctly.") dashboardPage.assertPageObjects() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/SyllabusE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/SyllabusE2ETest.kt index 8c97f277d1..39daa8c8be 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/SyllabusE2ETest.kt @@ -14,10 +14,10 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory @@ -29,8 +29,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/TodoE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/TodoE2ETest.kt index 8bac3991d3..896f7e2720 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/TodoE2ETest.kt @@ -1,11 +1,11 @@ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.refresh @@ -19,9 +19,9 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedAssignments -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedAssignments +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.lang.Thread.sleep diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/GradesElementaryE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/GradesElementaryE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/GradesElementaryE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/GradesElementaryE2ETest.kt index 431775a065..64ca48fb25 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/GradesElementaryE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/GradesElementaryE2ETest.kt @@ -14,16 +14,16 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.k5 +package com.instructure.student.ui.e2e.classic.k5 import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.GradingPeriodsApi import com.instructure.dataseeding.api.SubmissionsApi @@ -34,10 +34,10 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.page.getStringFromResource import com.instructure.student.R -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedDataForK5 -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.seedDataForK5 +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ImportantDatesE2ETest.kt similarity index 93% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ImportantDatesE2ETest.kt index 5c7be151e0..685cfb37fd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ImportantDatesE2ETest.kt @@ -14,33 +14,34 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.k5 +package com.instructure.student.ui.e2e.classic.k5 import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvasapi2.utils.toDate import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.pages.ElementaryDashboardPage -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedDataForK5 -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.extensions.seedDataForK5 +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale @HiltAndroidTest -class ImportantDatesE2ETest : StudentTest() { +class ImportantDatesE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ScheduleE2ETest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ScheduleE2ETest.kt index 04dc448f9a..54f76a8ea3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/k5/ScheduleE2ETest.kt @@ -14,18 +14,17 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.k5 +package com.instructure.student.ui.e2e.classic.k5 import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvasapi2.type.AssetString +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType @@ -33,10 +32,10 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.espresso.page.getStringFromResource import com.instructure.espresso.page.withAncestor import com.instructure.student.R -import com.instructure.student.ui.pages.ElementaryDashboardPage -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedDataForK5 -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.extensions.seedDataForK5 +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Rule import org.junit.Test @@ -47,7 +46,7 @@ import java.util.Locale import java.util.TimeZone @HiltAndroidTest -class ScheduleE2ETest : StudentTest() { +class ScheduleE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/ManageOfflineContentE2ETest.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/ManageOfflineContentE2ETest.kt index 55285c67ab..9e0ad68804 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/ManageOfflineContentE2ETest.kt @@ -14,22 +14,21 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvasapi2.type.AssetString -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAllCoursesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAllCoursesE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAllCoursesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAllCoursesE2ETest.kt index 1529e89278..b4267ddba6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAllCoursesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAllCoursesE2ETest.kt @@ -14,22 +14,22 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAnnouncementsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAnnouncementsE2ETest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAnnouncementsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAnnouncementsE2ETest.kt index f5efcd913e..fa6e4c2775 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAnnouncementsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAnnouncementsE2ETest.kt @@ -14,22 +14,22 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.DiscussionTopicsApi -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAssignmentsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAssignmentsE2ETest.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAssignmentsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAssignmentsE2ETest.kt index bc62e00f39..339e3aba5b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAssignmentsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineAssignmentsE2ETest.kt @@ -14,17 +14,18 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.AssignmentGroupsApi import com.instructure.dataseeding.api.AssignmentsApi @@ -34,11 +35,10 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.espresso.ViewUtils -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test @@ -235,7 +235,7 @@ class OfflineAssignmentsE2ETest : StudentComposeTest() { OfflineTestUtils.dismissNoInternetConnectionDialog() Log.d(STEP_TAG, "Navigate back to the Assignment List Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Click on the '${notSubmittedAssignment.name}' NOT submitted assignment.") assignmentListPage.clickAssignment(notSubmittedAssignment) @@ -254,7 +254,7 @@ class OfflineAssignmentsE2ETest : StudentComposeTest() { submissionDetailsPage.assertNoSubmissionEmptyView() Log.d(STEP_TAG, "Navigate back to the Assignment List Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Click on the '${gradedAssignment.name}' GRADED submitted assignment.") assignmentListPage.clickAssignment(gradedAssignment) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineConferencesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineConferencesE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineConferencesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineConferencesE2ETest.kt index f3dd1d7fbe..b9bc799389 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineConferencesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineConferencesE2ETest.kt @@ -14,22 +14,22 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.dataseeding.api.ConferencesApi -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertOfflineIndicator +import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineCourseBrowserE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineCourseBrowserE2ETest.kt index e17d4301c6..8ebc0ab19e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineCourseBrowserE2ETest.kt @@ -14,21 +14,21 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.pages.CourseBrowserPage +import com.instructure.canvas.espresso.annotations.OfflineE2E +import com.instructure.student.ui.pages.classic.CourseBrowserPage import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDashboardE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDashboardE2ETest.kt index 65093f02b9..b7f33e4fba 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDashboardE2ETest.kt @@ -14,23 +14,23 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils +import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDiscussionsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDiscussionsE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDiscussionsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDiscussionsE2ETest.kt index 0e204cb712..321952c93c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDiscussionsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineDiscussionsE2ETest.kt @@ -14,28 +14,28 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.checkToastText +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.espresso.getDateInCanvasFormat import com.instructure.student.R -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.openOverflowMenu -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.openOverflowMenu +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test @@ -89,7 +89,7 @@ class OfflineDiscussionsE2ETest : StudentTest() { discussionDetailsPage.assertEntryDisplayed("My reply") Log.d(STEP_TAG, "Navigate back to the Dashboard page.") - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Open the '${course.name}' course's 'Manage Offline Content' page via the more menu of the Dashboard Page.") dashboardPage.clickCourseOverflowMenu(course.name, "Manage Offline Content") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineFilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineFilesE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineFilesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineFilesE2ETest.kt index dfd96a4b1c..5546afc96e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineFilesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineFilesE2ETest.kt @@ -14,24 +14,24 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.dataseeding.api.FileFolderApi import com.instructure.dataseeding.model.FileUploadType -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin -import com.instructure.student.ui.utils.uploadTextFile +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.extensions.uploadTextFile +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineGradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineGradesE2ETest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineGradesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineGradesE2ETest.kt index 925461cbda..8e6e10ff9f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineGradesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineGradesE2ETest.kt @@ -14,17 +14,17 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.QuizzesApi @@ -37,16 +37,16 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.getDateInCanvasCalendarFormat -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test @HiltAndroidTest -class OfflineGradesE2ETest : StudentTest() { +class OfflineGradesE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLeftSideMenuE2ETest.kt similarity index 89% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLeftSideMenuE2ETest.kt index c9285237ff..c48435baa6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLeftSideMenuE2ETest.kt @@ -14,23 +14,23 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertNoInternetConnectionDialog -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.dismissNoInternetConnectionDialog import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertNoInternetConnectionDialog +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertOfflineIndicator +import com.instructure.student.ui.utils.offline.OfflineTestUtils.dismissNoInternetConnectionDialog import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLoginE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLoginE2ETest.kt similarity index 93% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLoginE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLoginE2ETest.kt index 753b40d2d8..9ac702bffd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLoginE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineLoginE2ETest.kt @@ -14,22 +14,22 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.model.CanvasUserApiModel -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertNoInternetConnectionDialog -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.dismissNoInternetConnectionDialog -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertNoInternetConnectionDialog +import com.instructure.student.ui.utils.offline.OfflineTestUtils.dismissNoInternetConnectionDialog +import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineModulesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineModulesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt index 1e7a7968db..00754ec014 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineModulesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineModulesE2ETest.kt @@ -14,16 +14,16 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.DiscussionTopicsApi @@ -36,18 +36,18 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertOfflineIndicator +import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test @HiltAndroidTest -class OfflineModulesE2ETest : StudentTest() { +class OfflineModulesE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePagesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePagesE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePagesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePagesE2ETest.kt index 2d3411748a..1d7dbfe403 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePagesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePagesE2ETest.kt @@ -14,27 +14,27 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.PagesApi -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertOfflineIndicator +import com.instructure.student.ui.utils.offline.OfflineTestUtils.waitForNetworkToGoOffline import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePeopleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePeopleE2ETest.kt similarity index 92% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePeopleE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePeopleE2ETest.kt index 519ca1e92c..6d6eb60128 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflinePeopleE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflinePeopleE2ETest.kt @@ -14,21 +14,21 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.matcher.ViewMatchers import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSettingsE2ETest.kt similarity index 88% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSettingsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSettingsE2ETest.kt index 6955501151..2cab83b195 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSettingsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSettingsE2ETest.kt @@ -14,23 +14,23 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertNoInternetConnectionDialog -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.dismissNoInternetConnectionDialog import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertNoInternetConnectionDialog +import com.instructure.student.ui.utils.offline.OfflineTestUtils.assertOfflineIndicator +import com.instructure.student.ui.utils.offline.OfflineTestUtils.dismissNoInternetConnectionDialog import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyllabusE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyllabusE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyllabusE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyllabusE2ETest.kt index 8953e14e2c..8969573d0a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyllabusE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyllabusE2ETest.kt @@ -14,15 +14,15 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.SubmissionType @@ -30,10 +30,10 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.retryWithIncreasingDelay -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncProgressE2ETest.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncProgressE2ETest.kt index 6dfa04fd8f..0c21032139 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncProgressE2ETest.kt @@ -14,23 +14,23 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import androidx.test.espresso.Espresso import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.refresh -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.offline.OfflineTestUtils import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncSettingsE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncSettingsE2ETest.kt index 9464b4ec0e..ccbf589970 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/offline/OfflineSyncSettingsE2ETest.kt @@ -14,20 +14,20 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline +package com.instructure.student.ui.e2e.classic.offline import android.util.Log import com.instructure.canvas.espresso.FeatureCategory -import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.OfflineE2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.pandautils.R import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Test @@ -107,7 +107,7 @@ class OfflineSyncSettingsE2ETest : StudentComposeTest() { offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.weekly) Log.d(STEP_TAG, "Navigate back to Dashboard Page and logout.") - ViewUtils.pressBackButton(2) + pressBackButton(2) leftSideNavigationDrawerPage.logout() Log.d(STEP_TAG, "Click 'Find My School' button.") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/usergroups/UserGroupFilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/usergroups/UserGroupFilesE2ETest.kt similarity index 89% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/usergroups/UserGroupFilesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/usergroups/UserGroupFilesE2ETest.kt index 780bc91011..27d0e6ea14 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/usergroups/UserGroupFilesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/classic/usergroups/UserGroupFilesE2ETest.kt @@ -14,21 +14,23 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.usergroups +package com.instructure.student.ui.e2e.classic.usergroups import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.intent.Intents -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.GroupsApi +import com.instructure.espresso.handleWorkManagerTask +import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -105,8 +107,13 @@ class UserGroupFilesE2ETest : StudentTest() { } fileChooserPage.clickUpload() - Log.d(ASSERTION_TAG, "Assert that the file upload was successful.") - fileListPage.assertItemDisplayed("samplepdf.pdf") + retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { + handleWorkManagerTask("FileUploadWorker", 20000) + }) + { + Log.d(ASSERTION_TAG, "Assert that the file upload was successful.") + fileListPage.assertItemDisplayed("samplepdf.pdf") + } Log.d(STEP_TAG, "Navigate back to File List Page.") Espresso.pressBack() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/AssignmentsE2ETest.kt similarity index 93% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/AssignmentsE2ETest.kt index 40942aa0f4..bc5497b79b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/AssignmentsE2ETest.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.compose import android.os.SystemClock.sleep import android.util.Log @@ -22,15 +22,16 @@ import android.view.KeyEvent import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers import androidx.test.rule.GrantPermissionRule -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.checkToastText import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage +import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.AssignmentGroupsApi import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.CoursesApi @@ -42,20 +43,23 @@ import com.instructure.dataseeding.util.ago import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 +import com.instructure.espresso.handleWorkManagerTask import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.pandautils.utils.toFormattedString import com.instructure.student.R import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin -import com.instructure.student.ui.utils.uploadTextFile +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.extensions.uploadTextFile import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.FixMethodOrder import org.junit.Rule import org.junit.Test +import org.junit.runners.MethodSorters import java.util.Calendar @HiltAndroidTest +@FixMethodOrder(MethodSorters.NAME_ASCENDING) class AssignmentsE2ETest: StudentComposeTest() { override fun displaysPageObjects() = Unit @@ -69,6 +73,117 @@ class AssignmentsE2ETest: StudentComposeTest() { android.Manifest.permission.CAMERA ) + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.SUBMISSIONS, TestCategory.E2E) + fun test01CommentsBelongToSubmissionAttempts() { + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(teachers = 1, courses = 1, students = 1) + val student = data.studentsList[0] + val teacher = data.teachersList[0] + val course = data.coursesList[0] + + Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.") + val pointsTextAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) + + Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Select course: '${course.name}'.") + dashboardPage.selectCourse(course) + + Log.d(STEP_TAG, "Navigate to course Assignments Page.") + courseBrowserPage.selectAssignments() + + Log.d(ASSERTION_TAG, "Assert that our assignments are present, along with any grade/date info.") + assignmentListPage.assertHasAssignment(pointsTextAssignment) + + Log.d(STEP_TAG, "Click on assignment '${pointsTextAssignment.name}'.") + assignmentListPage.clickAssignment(pointsTextAssignment) + + Log.d(ASSERTION_TAG, "Assert that the corresponding views are displayed on the Assignment Details Page, and there is no submission yet.") + assignmentDetailsPage.assertPageObjects() + assignmentDetailsPage.assertStatusNotSubmitted() + + Log.d(ASSERTION_TAG, "Assert that 'Submission & Rubric' label is displayed and navigate to Submission Details Page.") + assignmentDetailsPage.assertSubmissionAndRubricLabel() + assignmentDetailsPage.goToSubmissionDetails() + + Log.d(ASSERTION_TAG, "Assert that there is no submission yet for the '${pointsTextAssignment.name}' assignment.") + submissionDetailsPage.assertNoSubmissionEmptyView() + + Log.d(STEP_TAG, "Navigate back to Assignment Details page.") + Espresso.pressBack() + + Log.d(PREPARATION_TAG, "Submit assignment: '${pointsTextAssignment.name}' for student: '${student.name}'.") + SubmissionsApi.seedAssignmentSubmission(course.id, student.token, pointsTextAssignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY))) + + Log.d(ASSERTION_TAG, "Refresh the Assignment Details Page. Assert that the assignment's status is submitted and the 'Submission and Rubric' label is displayed.") + assignmentDetailsPage.refresh() + assignmentDetailsPage.assertStatusSubmitted() + assignmentDetailsPage.assertSubmissionAndRubricLabel() + + Log.d(PREPARATION_TAG, "Make another submission for assignment: '${pointsTextAssignment.name}' for student: '${student.name}'.") + val secondSubmissionAttempt = SubmissionsApi.seedAssignmentSubmission(course.id, student.token, pointsTextAssignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY))) + + Log.d(ASSERTION_TAG, "Refresh the Assignment Details Page. Assert that the assignment's status is submitted and the 'Submission and Rubric' label is displayed.") + assignmentDetailsPage.refresh() + assignmentDetailsPage.assertStatusSubmitted() + assignmentDetailsPage.assertSubmissionAndRubricLabel() + + Log.d(ASSERTION_TAG, "Assert that the spinner is displayed and the last/newest attempt is selected.") + assignmentDetailsPage.assertAttemptSpinnerDisplayed() + assignmentDetailsPage.assertAttemptInformation() + assignmentDetailsPage.assertSelectedAttempt(2) + + Log.d(STEP_TAG, "Navigate to submission details Comments Tab.") + assignmentDetailsPage.goToSubmissionDetails() + submissionDetailsPage.openComments() + + Log.d(ASSERTION_TAG, "Assert that '${secondSubmissionAttempt[0].body}' text submission has been displayed as a comment.") + submissionDetailsPage.assertTextSubmissionDisplayedAsComment() + + val newComment = "Comment for second attempt" + Log.d(STEP_TAG, "Add a new comment ('$newComment') and send it.") + submissionDetailsPage.addAndSendComment(newComment) + handleWorkManagerTask("SubmissionWorker") + + Log.d(ASSERTION_TAG, "Assert that '$newComment' is displayed.") + submissionDetailsPage.assertCommentDisplayed(newComment, student) + + Log.d(STEP_TAG, "Select 'Attempt 1'.") + submissionDetailsPage.selectAttempt("Attempt 1") + + Log.d(ASSERTION_TAG, "Assert that the selected attempt is 'Attempt 1'.") + submissionDetailsPage.assertSelectedAttempt("Attempt 1") + + Log.d(STEP_TAG, "Open 'Comments' tab.") + submissionDetailsPage.openComments() + + Log.d(ASSERTION_TAG, "Assert that '$newComment' is NOT displayed because it belongs to 'Attempt 2'.") + submissionDetailsPage.assertCommentNotDisplayed(newComment, student) + + Log.d(ASSERTION_TAG, "Assert that '${secondSubmissionAttempt[0].body}' text submission has been displayed as a comment.") + submissionDetailsPage.assertTextSubmissionDisplayedAsComment() + + Log.d(STEP_TAG, "Select 'Attempt 2'.") + submissionDetailsPage.selectAttempt("Attempt 2") + + Log.d(ASSERTION_TAG, "Assert that the selected attempt is 'Attempt 2'.") + submissionDetailsPage.assertSelectedAttempt("Attempt 2") + + Log.d(STEP_TAG, "Open 'Comments' tab.") + submissionDetailsPage.openComments() + + Log.d(ASSERTION_TAG, "Assert that '$newComment' is displayed because it belongs to 'Attempt 2'.") + submissionDetailsPage.assertCommentDisplayed(newComment, student) + + Log.d(ASSERTION_TAG, "Assert that '${secondSubmissionAttempt[0].body}' text submission has been displayed as a comment.") + submissionDetailsPage.assertTextSubmissionDisplayedAsComment() + } + @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.E2E, SecondaryFeatureCategory.ASSIGNMENT_REMINDER) @@ -105,72 +220,72 @@ class AssignmentsE2ETest: StudentComposeTest() { Log.d(ASSERTION_TAG, "Assert that the corresponding views are displayed on the Assignment Details Page." + "Assert that the reminder section is displayed as well.") assignmentDetailsPage.assertPageObjects() - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneHour = futureDueDate.apply { add(Calendar.HOUR, -1) } Log.d(STEP_TAG, "Select '1 Hour Before'.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneHour) - reminderPage.selectTime(reminderDateOneHour) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneHour) + assignmentReminderPage.selectTime(reminderDateOneHour) Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Hour Before' again.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneHour) - reminderPage.selectTime(reminderDateOneHour) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneHour) + assignmentReminderPage.selectTime(reminderDateOneHour) Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice.") checkToastText(R.string.reminderAlreadySet, activityRule.activity) Log.d(STEP_TAG, "Remove the '1 Hour Before' reminder, confirm the deletion dialog.") - reminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) Log.d(ASSERTION_TAG, "Assert that the '1 Hour Before' reminder is not displayed any more.") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) futureDueDate.apply { add(Calendar.HOUR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneWeek = futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, -1) } Log.d(STEP_TAG, "Select '1 Week Before'.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneWeek) - reminderPage.selectTime(reminderDateOneWeek) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneWeek) + assignmentReminderPage.selectTime(reminderDateOneWeek) Log.d(ASSERTION_TAG, "Assert that the '1 Week Before' reminder is not displayed, as it is in the past." + "Assert that a toast message is occurring which warns that we cannot pick up a reminder which has already passed (for example cannot pick '1 Week Before' reminder for an assignment which ends tomorrow).") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) checkToastText(R.string.reminderInPast, activityRule.activity) futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneDay = futureDueDate.apply { add(Calendar.DAY_OF_MONTH, -1) } Log.d(STEP_TAG, "Select '1 Day Before'.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneDay) - reminderPage.selectTime(reminderDateOneDay) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneDay) + assignmentReminderPage.selectTime(reminderDateOneDay) Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder and select '1 Day Before' again with custom date and time.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Day Before' again.") - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderDateOneDay) - reminderPage.selectTime(reminderDateOneDay) + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderDateOneDay) + assignmentReminderPage.selectTime(reminderDateOneDay) Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice. (Because 1 days and 24 hours is the same)") checkToastText(R.string.reminderAlreadySet, activityRule.activity) @@ -178,22 +293,22 @@ class AssignmentsE2ETest: StudentComposeTest() { futureDueDate.apply { add(Calendar.DAY_OF_MONTH, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder and select the custom reminder option and select date and time.") - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(futureDate) - reminderPage.selectCustomTime(KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_0) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(futureDate) + assignmentReminderPage.selectCustomTime(KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_0) Log.d(ASSERTION_TAG, "Assert that the 'Invalid time' error is shown when we typed '0' hour and '0' minutes for the custom reminder.") - reminderPage.assertInvalidTimeShown() + assignmentReminderPage.assertInvalidTimeShown() Log.d(STEP_TAG, "Navigate back to Assignment List Page.") - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Click on assignment '${alreadyPastAssignment.name}'.") assignmentListPage.clickAssignment(alreadyPastAssignment) Log.d(ASSERTION_TAG, "Assert that the reminder section is NOT displayed, because the '${alreadyPastAssignment.name}' assignment has already passed..") - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @E2E @@ -229,64 +344,64 @@ class AssignmentsE2ETest: StudentComposeTest() { Log.d(ASSERTION_TAG, "Assert that the corresponding views are displayed on the Assignment Details Page. Assert that the reminder section is displayed as well.") assignmentDetailsPage.assertPageObjects() - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneHour = futureDueDate.apply { add(Calendar.HOUR, -1) } Log.d(STEP_TAG, "Select '1 Hour Before'.") - reminderPage.clickBeforeReminderOption("1 Hour Before") + assignmentReminderPage.clickBeforeReminderOption("1 Hour Before") Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneHour.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Hour Before' again.") - reminderPage.clickBeforeReminderOption("1 Hour Before") + assignmentReminderPage.clickBeforeReminderOption("1 Hour Before") Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice.") checkToastText(R.string.reminderAlreadySet, activityRule.activity) Log.d(STEP_TAG, "Remove the '1 Hour Before' reminder, confirm the deletion dialog.") - reminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.removeReminderWithText(reminderDateOneHour.time.toFormattedString()) Log.d(ASSERTION_TAG, "Assert that the '1 Hour Before' reminder is not displayed any more.") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneHour.time.toFormattedString()) futureDueDate.apply { add(Calendar.HOUR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneWeek = futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, -1) } Log.d(STEP_TAG, "Select '1 Week Before'.") - reminderPage.clickBeforeReminderOption("1 Week Before") + assignmentReminderPage.clickBeforeReminderOption("1 Week Before") Log.d(ASSERTION_TAG, "Assert that the '1 Week Before' reminder is not displayed, as it is in the past." + "Assert that a toast message is occurring which warns that we cannot pick up a reminder which has already passed (for example cannot pick '1 Week Before' reminder for an assignment which ends tomorrow).") - reminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderDateOneWeek.time.toFormattedString()) composeTestRule.waitForIdle() checkToastText(R.string.reminderInPast, activityRule.activity) composeTestRule.waitForIdle() futureDueDate.apply { add(Calendar.WEEK_OF_YEAR, 1) } Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() val reminderDateOneDay = futureDueDate.apply { add(Calendar.DAY_OF_MONTH, -1) } Log.d(STEP_TAG, "Select '1 Day Before'.") - reminderPage.clickBeforeReminderOption("1 Day Before") + assignmentReminderPage.clickBeforeReminderOption("1 Day Before") Log.d(ASSERTION_TAG, "Assert that the reminder has been picked up and displayed on the Assignment Details Page.") - reminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderDateOneDay.time.toFormattedString()) Log.d(STEP_TAG, "Click on the '+' button (Add reminder) to pick up a new reminder.") - reminderPage.clickAddReminder() + assignmentReminderPage.clickAddReminder() Log.d(STEP_TAG, "Select '1 Day Before' again.") - reminderPage.clickBeforeReminderOption("1 Day Before") + assignmentReminderPage.clickBeforeReminderOption("1 Day Before") Log.d(ASSERTION_TAG, "Assert that a toast message is occurring which warns that we cannot pick up the same time reminder twice. (Because 1 days and 24 hours is the same)") composeTestRule.waitForIdle() @@ -302,7 +417,7 @@ class AssignmentsE2ETest: StudentComposeTest() { assignmentListPage.clickAssignment(alreadyPastAssignment) Log.d(ASSERTION_TAG, "Assert that the reminder section is NOT displayed, because the '${alreadyPastAssignment.name}' assignment has already passed..") - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @E2E @@ -806,123 +921,10 @@ class AssignmentsE2ETest: StudentComposeTest() { submissionDetailsPage.assertSelectedAttempt("Attempt 1") } - @Stub - @E2E - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.SUBMISSIONS, TestCategory.E2E) - fun testCommentsBelongToSubmissionAttempts() { - - Log.d(PREPARATION_TAG, "Seeding data.") - val data = seedData(teachers = 1, courses = 1, students = 1) - val student = data.studentsList[0] - val teacher = data.teachersList[0] - val course = data.coursesList[0] - - Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.") - val pointsTextAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) - - Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.") - tokenLogin(student) - dashboardPage.waitForRender() - - Log.d(STEP_TAG, "Select course: '${course.name}'.") - dashboardPage.selectCourse(course) - - Log.d(STEP_TAG, "Navigate to course Assignments Page.") - courseBrowserPage.selectAssignments() - - Log.d(ASSERTION_TAG, "Assert that our assignments are present, along with any grade/date info.") - assignmentListPage.assertHasAssignment(pointsTextAssignment) - - Log.d(STEP_TAG, "Click on assignment '${pointsTextAssignment.name}'.") - assignmentListPage.clickAssignment(pointsTextAssignment) - - Log.d(ASSERTION_TAG, "Assert that the corresponding views are displayed on the Assignment Details Page, and there is no submission yet.") - assignmentDetailsPage.assertPageObjects() - assignmentDetailsPage.assertStatusNotSubmitted() - - Log.d(ASSERTION_TAG, "Assert that 'Submission & Rubric' label is displayed and navigate to Submission Details Page.") - assignmentDetailsPage.assertSubmissionAndRubricLabel() - assignmentDetailsPage.goToSubmissionDetails() - - Log.d(ASSERTION_TAG, "Assert that there is no submission yet for the '${pointsTextAssignment.name}' assignment.") - submissionDetailsPage.assertNoSubmissionEmptyView() - - Log.d(STEP_TAG, "Navigate back to Assignment Details page.") - Espresso.pressBack() - - Log.d(PREPARATION_TAG, "Submit assignment: '${pointsTextAssignment.name}' for student: '${student.name}'.") - SubmissionsApi.seedAssignmentSubmission(course.id, student.token, pointsTextAssignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY))) - - Log.d(ASSERTION_TAG, "Refresh the Assignment Details Page. Assert that the assignment's status is submitted and the 'Submission and Rubric' label is displayed.") - assignmentDetailsPage.refresh() - assignmentDetailsPage.assertStatusSubmitted() - assignmentDetailsPage.assertSubmissionAndRubricLabel() - - Log.d(PREPARATION_TAG, "Make another submission for assignment: '${pointsTextAssignment.name}' for student: '${student.name}'.") - val secondSubmissionAttempt = SubmissionsApi.seedAssignmentSubmission(course.id, student.token, pointsTextAssignment.id, submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo(amount = 1, submissionType = SubmissionType.ONLINE_TEXT_ENTRY))) - - Log.d(ASSERTION_TAG, "Refresh the Assignment Details Page. Assert that the assignment's status is submitted and the 'Submission and Rubric' label is displayed.") - assignmentDetailsPage.refresh() - assignmentDetailsPage.assertStatusSubmitted() - assignmentDetailsPage.assertSubmissionAndRubricLabel() - - Log.d(ASSERTION_TAG, "Assert that the spinner is displayed and the last/newest attempt is selected.") - assignmentDetailsPage.assertAttemptSpinnerDisplayed() - assignmentDetailsPage.assertAttemptInformation() - assignmentDetailsPage.assertSelectedAttempt(2) - - Log.d(STEP_TAG, "Navigate to submission details Comments Tab.") - assignmentDetailsPage.goToSubmissionDetails() - submissionDetailsPage.openComments() - - Log.d(ASSERTION_TAG, "Assert that '${secondSubmissionAttempt[0].body}' text submission has been displayed as a comment.") - submissionDetailsPage.assertTextSubmissionDisplayedAsComment() - - val newComment = "Comment for second attempt" - Log.d(STEP_TAG, "Add a new comment ('$newComment') and send it.") - submissionDetailsPage.addAndSendComment(newComment) - - sleep(2000) // Give the comment time to propagate - - Log.d(ASSERTION_TAG, "Assert that '$newComment' is displayed.") - submissionDetailsPage.assertCommentDisplayed(newComment, student) - - Log.d(STEP_TAG, "Select 'Attempt 1'.") - submissionDetailsPage.selectAttempt("Attempt 1") - - Log.d(ASSERTION_TAG, "Assert that the selected attempt is 'Attempt 1'.") - submissionDetailsPage.assertSelectedAttempt("Attempt 1") - - Log.d(STEP_TAG, "Open 'Comments' tab.") - submissionDetailsPage.openComments() - - Log.d(ASSERTION_TAG, "Assert that '$newComment' is NOT displayed because it belongs to 'Attempt 2'.") - submissionDetailsPage.assertCommentNotDisplayed(newComment, student) - - Log.d(ASSERTION_TAG, "Assert that '${secondSubmissionAttempt[0].body}' text submission has been displayed as a comment.") - submissionDetailsPage.assertTextSubmissionDisplayedAsComment() - - Log.d(STEP_TAG, "Select 'Attempt 2'.") - submissionDetailsPage.selectAttempt("Attempt 2") - - Log.d(ASSERTION_TAG, "Assert that the selected attempt is 'Attempt 2'.") - submissionDetailsPage.assertSelectedAttempt("Attempt 2") - - Log.d(STEP_TAG, "Open 'Comments' tab.") - submissionDetailsPage.openComments() - - Log.d(ASSERTION_TAG, "Assert that '$newComment' is displayed because it belongs to 'Attempt 2'.") - submissionDetailsPage.assertCommentDisplayed(newComment, student) - - Log.d(ASSERTION_TAG, "Assert that '${secondSubmissionAttempt[0].body}' text submission has been displayed as a comment.") - submissionDetailsPage.assertTextSubmissionDisplayedAsComment() - } - @E2E @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.E2E) - fun showOnlyLetterGradeOnDashboardAndAssignmentListPageE2E() { + fun testShowOnlyLetterGradeOnDashboardAndAssignmentListPageE2E() { Log.d(PREPARATION_TAG, "Seeding data.") val data = seedData(teachers = 1, courses = 1, students = 1) val student = data.studentsList[0] @@ -1115,7 +1117,7 @@ class AssignmentsE2ETest: StudentComposeTest() { Espresso.pressBack() Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(ASSERTION_TAG, "Assert that the course grade is F, as it is converted to letter grade because the disability of the restriction has not propagated yet.") dashboardPage.assertCourseGrade(course.name, "F") @@ -1128,7 +1130,7 @@ class AssignmentsE2ETest: StudentComposeTest() { @E2E @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.E2E) - fun showOnlyLetterGradeOnGradesPageE2E() { + fun testShowOnlyLetterGradeOnGradesPageE2E() { Log.d(PREPARATION_TAG, "Seeding data.") val data = seedData(teachers = 1, courses = 1, students = 1) val student = data.studentsList[0] @@ -1184,7 +1186,7 @@ class AssignmentsE2ETest: StudentComposeTest() { SubmissionsApi.gradeSubmission(teacher.token, course.id, passFailAssignment.id, student.id, postedGrade = "Incomplete") Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.") - val gpaScaleAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.GPA_SCALE, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) + val gpaScaleAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.GPA_SCALE, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) Log.d(PREPARATION_TAG, "Grade submission: '${gpaScaleAssignment.name}' with 3.7.") SubmissionsApi.gradeSubmission(teacher.token, course.id, gpaScaleAssignment.id, student.id, postedGrade = "3.7") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/BookmarksE2ETest.kt similarity index 64% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/BookmarksE2ETest.kt index 85c27a402b..4a91a876c8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/BookmarksE2ETest.kt @@ -14,17 +14,18 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.E2E +import androidx.test.uiautomator.UiSelector import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.refresh +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.PagesApi @@ -34,11 +35,10 @@ import com.instructure.dataseeding.model.UpdateCourse import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -77,7 +77,7 @@ class BookmarksE2ETest : StudentComposeTest() { assignmentDetailsPage.addBookmark(bookmarkName) Log.d(STEP_TAG, "Navigate back to Bookmarks page.") - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Click on the 'Bookmarks' menu within the left side menu to open the Bookmarks page.") leftSideNavigationDrawerPage.clickBookmarksMenu() @@ -91,21 +91,17 @@ class BookmarksE2ETest : StudentComposeTest() { Log.d(ASSERTION_TAG, "Assert if the '$bookmarkName' bookmark is navigating to the Assignment Details page.") assignmentDetailsPage.assertAssignmentTitle(assignment.name) - Log.d(STEP_TAG, "Navigate back to the Dashboard page.") + Log.d(STEP_TAG, "Navigate back to the Bookmark List page.") Espresso.pressBack() - Log.d(STEP_TAG, "Click on the bookmark page's overflow menu.") - leftSideNavigationDrawerPage.clickBookmarksMenu() - - Log.d(ASSERTION_TAG, "Assert if the bookmark is displayed.") + Log.d(ASSERTION_TAG, "Assert if the '${bookmarkName}' bookmark is displayed.") bookmarkPage.assertBookmarkDisplayed(bookmarkName) val newName = "Assignment Details BM Modified" Log.d(STEP_TAG, "Change bookmark's name from '$bookmarkName' to '$newName'.") bookmarkPage.changeBookmarkName(bookmarkName, newName) - Log.d(ASSERTION_TAG, "Refresh bookmark page and assert if the bookmark's name has been changed.") - refresh() + Log.d(ASSERTION_TAG, "Assert if the '${bookmarkName}' bookmark's name has been changed to '$newName'.") bookmarkPage.assertBookmarkDisplayed(newName) Log.d(STEP_TAG, "Click on the previously renamed bookmark.") @@ -114,12 +110,9 @@ class BookmarksE2ETest : StudentComposeTest() { Log.d(ASSERTION_TAG, "Assert if it's still navigating to the corresponding assignment's details page.") assignmentDetailsPage.assertAssignmentTitle(assignment.name) - Log.d(STEP_TAG, "Navigate back to the bookmark page.") + Log.d(STEP_TAG, "Navigate back to the Bookmark List page.") Espresso.pressBack() - Log.d(STEP_TAG, "Click on the bookmark page's overflow menu.") - leftSideNavigationDrawerPage.clickBookmarksMenu() - Log.d(STEP_TAG, "Delete bookmark: '$newName'.") bookmarkPage.deleteBookmark(newName) @@ -160,7 +153,7 @@ class BookmarksE2ETest : StudentComposeTest() { assignmentDetailsPage.addBookmark(bookmarkName) Log.d(STEP_TAG, "Navigate back to the Dashboard page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Click on the 'Bookmarks' menu within the left side menu to open the Bookmarks page.") leftSideNavigationDrawerPage.clickBookmarksMenu() @@ -175,4 +168,64 @@ class BookmarksE2ETest : StudentComposeTest() { canvasWebViewPage.runTextChecks(WebViewTextCheck(Locator.ID, "header1", "Front Page Text")) } + @E2E + @Test + @TestMetaData(Priority.COMMON, FeatureCategory.BOOKMARKS, TestCategory.E2E) + fun testBookmarkAddToHomeScreenE2E() { + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1) + val student = data.studentsList[0] + val teacher = data.teachersList[0] + val course = data.coursesList[0] + + Log.d(PREPARATION_TAG, "Preparing an assignment which will be saved as a bookmark.") + val assignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.POINTS, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) + + Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Navigate to assignments page and click on the prepared assignment: '${assignment.name}'.") + dashboardPage.selectCourse(course) + courseBrowserPage.selectAssignments() + assignmentListPage.clickAssignment(assignment) + + val bookmarkName = "Assignment bookmark" + Log.d(STEP_TAG, "Add a new bookmark with name: '$bookmarkName'") + assignmentDetailsPage.addBookmark(bookmarkName) + + Log.d(STEP_TAG, "Navigate back to Dashboard page.") + pressBackButton(3) + + Log.d(STEP_TAG, "Click on the 'Bookmarks' menu within the left side menu to open the Bookmarks page.") + leftSideNavigationDrawerPage.clickBookmarksMenu() + + Log.d(ASSERTION_TAG, "Assert if the newly created bookmark: '$bookmarkName' has displayed.") + bookmarkPage.assertBookmarkDisplayed(bookmarkName) + + Log.d(STEP_TAG, "Click on 'Add to Home Screen' option for the bookmark: '$bookmarkName'.") + bookmarkPage.addBookmarkToHomeScreen(bookmarkName, device) + + Log.d(ASSERTION_TAG, "Assert that we're still on the bookmarks page after adding the bookmark: '$bookmarkName' to home screen.") + bookmarkPage.assertBookmarkDisplayed(bookmarkName) + + Log.d(STEP_TAG, "Press 'Home' button to go to home screen.") + device.pressHome() + + Log.d(STEP_TAG, "Find and click the '$bookmarkName' shortcut on the home screen.") + device.findObject(UiSelector().textContains(bookmarkName)).click() + + Log.d(ASSERTION_TAG, "Assert that the bookmark shortcut opened the correct assignment: '${assignment.name}' with all details.") + assignmentDetailsPage.assertAssignmentTitle(assignment.name) + assignmentDetailsPage.assertAssignmentDetails(assignment) + + Log.d(STEP_TAG, "Navigate back to the system home screen.") + device.pressBack() + + Log.d(ASSERTION_TAG, "Assert that the app returned to the system home screen by verifying the bookmark shortcut is visible.") + val homeScreenShortcut = device.findObject(UiSelector().textContains(bookmarkName)) + assert(homeScreenShortcut.exists()) { "Expected to be on system home screen with bookmark shortcut visible, but it was not found." } + } + } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CalendarE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CalendarE2ETest.kt index e403b0d867..661fd12c56 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CalendarE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CalendarE2ETest.kt @@ -16,16 +16,16 @@ package com.instructure.student.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.espresso.getDateInCanvasCalendarFormat import com.instructure.pandautils.features.calendar.CalendarPrefs import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CourseBrowserE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CourseBrowserE2ETest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CourseBrowserE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CourseBrowserE2ETest.kt index 4e2544ae53..56c7d47000 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CourseBrowserE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/CourseBrowserE2ETest.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.compose import android.util.Log import androidx.compose.ui.test.assertIsDisplayed @@ -24,12 +24,12 @@ import androidx.compose.ui.test.performImeAction import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.requestFocus import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvasapi2.models.SmartSearchContentType import com.instructure.canvasapi2.models.SmartSearchFilter import com.instructure.dataseeding.api.AssignmentsApi @@ -44,8 +44,8 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/InboxE2ETest.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/InboxE2ETest.kt index cb5a5833ce..fe3596f126 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/InboxE2ETest.kt @@ -14,25 +14,25 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.compose import android.os.SystemClock.sleep import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.ReleaseExclude import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.ReleaseExclude import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.GroupsApi import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/PeopleE2ETest.kt similarity index 93% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/PeopleE2ETest.kt index 639524adf7..a08306675b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/PeopleE2ETest.kt @@ -14,18 +14,18 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -95,7 +95,7 @@ class PeopleE2ETest : StudentComposeTest() { composeTestRule.waitForIdle() Log.d(STEP_TAG, "Navigate back to the Dashboard (Course List) Page.") - ViewUtils.pressBackButton(3) + pressBackButton(3) composeTestRule.waitForIdle() Log.d(STEP_TAG, "Sign out with '${student1.name}' student.") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/SettingsE2ETest.kt similarity index 79% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/SettingsE2ETest.kt index 404cce6edf..c15553f422 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/SettingsE2ETest.kt @@ -14,34 +14,34 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e +package com.instructure.student.ui.e2e.compose import android.content.Intent import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intended -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory +import com.instructure.canvas.espresso.IntentActionMatcher import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.checkToastText +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvasapi2.utils.RemoteConfigParam import com.instructure.canvasapi2.utils.RemoteConfigUtils import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.EnrollmentsApi import com.instructure.dataseeding.util.CanvasNetworkAdapter -import com.instructure.espresso.ViewUtils import com.instructure.pandautils.utils.AppTheme import com.instructure.student.BuildConfig import com.instructure.student.R -import com.instructure.student.ui.utils.IntentActionMatcher import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.seedData +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After import org.junit.Assert @@ -89,7 +89,7 @@ class SettingsE2ETest : StudentComposeTest() { profileSettingsPage.changeUserNameTo(newUserName) Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(ASSERTION_TAG, "Assert that the Dashboard Page has been displayed correctly.") leftSideNavigationDrawerPage.assertUserLoggedIn(newUserName) @@ -428,7 +428,7 @@ class SettingsE2ETest : StudentComposeTest() { CoursesApi.concludeCourse(course.id) // Need to conclude the course because otherwise there would be too much course with time on the dedicated user's dashboard. Log.d(STEP_TAG, "Navigate back to Dashboard.") - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Open the Left Side Menu.") dashboardPage.openLeftSideMenu() @@ -545,7 +545,7 @@ class SettingsE2ETest : StudentComposeTest() { inboxSignatureSettingsPage.assertSignatureEnabledState(true) Log.d(STEP_TAG, "Navigate back to the Dashboard.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open Inbox Page.") dashboardPage.clickInboxTab() @@ -652,7 +652,7 @@ class SettingsE2ETest : StudentComposeTest() { inboxComposePage.assertBodyText("") Log.d(STEP_TAG, "Navigate back to the Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open the Left Side Navigation Drawer menu.") dashboardPage.openLeftSideMenu() @@ -695,7 +695,7 @@ class SettingsE2ETest : StudentComposeTest() { inboxComposePage.assertBodyText("\n\n---\nLoyal member of Instructure") Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Click on 'Change User' button on the Left Side Navigation Drawer menu.") leftSideNavigationDrawerPage.clickChangeUserMenu() @@ -712,4 +712,177 @@ class SettingsE2ETest : StudentComposeTest() { Log.d(ASSERTION_TAG, "Assert that the previously set inbox signature text is displayed by default since we logged back with '${student.name}' student.") inboxComposePage.assertBodyText("\n\n---\nPresident of AC Milan\nVice President of Ferencvaros") } + + @E2E + @Test + @TestMetaData(Priority.BUG_CASE, FeatureCategory.INBOX, TestCategory.E2E, SecondaryFeatureCategory.INBOX_SIGNATURE) + fun testInboxSignaturesFABButtonWithDifferentUsersE2E() { + + //Bug Ticket: MBL-18929 + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 2, teachers = 1 ,courses = 1) + val student = data.studentsList[0] + val student2 = data.studentsList[1] + val teacher = data.teachersList[0] + val course = data.coursesList[0] + + Log.d(STEP_TAG, "Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG, "Enter domain: '${CanvasNetworkAdapter.canvasDomain}'") + loginFindSchoolPage.enterDomain(CanvasNetworkAdapter.canvasDomain) + + Log.d(STEP_TAG, "Click on 'Next' button on the toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(STEP_TAG, "Login with user: '${student.name}', login id: '${student.loginId}'.") + loginSignInPage.loginAs(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open the Left Side Navigation Drawer menu.") + dashboardPage.openLeftSideMenu() + + Log.d(STEP_TAG, "Navigate to Settings Page on the left-side menu.") + leftSideNavigationDrawerPage.clickSettingsMenu() + + Log.d(ASSERTION_TAG, "Assert that by default the Inbox Signature is 'Not Set'.") + settingsPage.assertSettingsItemDisplayed("Inbox Signature", "Not Set") + + Log.d(STEP_TAG, "Click on the 'Inbox Signature' settings.") + settingsPage.clickOnSettingsItem("Inbox Signature") + + Log.d(ASSERTION_TAG, "Assert that by default the 'Inbox Signature' toggle is turned off.") + inboxSignatureSettingsPage.assertSignatureEnabledState(false) + + val signatureText = "Best Regards\nCanvas Student" + Log.d(STEP_TAG, "Turn on the 'Inbox Signature' and set the inbox signature text to: '$signatureText'. Save the changes.") + inboxSignatureSettingsPage.toggleSignatureEnabledState() + inboxSignatureSettingsPage.changeSignatureText(signatureText) + inboxSignatureSettingsPage.saveChanges() + + Log.d(ASSERTION_TAG, "Assert that the 'Inbox settings saved!' toast message is displayed.") + checkToastText(R.string.inboxSignatureSettingsUpdated, activityRule.activity) + + Log.d(STEP_TAG, "Refresh the Settings page.") + settingsPage.refresh() + + Log.d(ASSERTION_TAG, "Assert that the Inbox Signature became 'Enabled'.") + settingsPage.assertSettingsItemDisplayed("Inbox Signature", "Enabled") + + Log.d(STEP_TAG, "Navigate back to the Dashboard.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Select course: '${course.name}'.") + dashboardPage.selectCourse(course) + + Log.d(STEP_TAG, "Navigate to People page.") + courseBrowserPage.selectPeople() + + Log.d(STEP_TAG, "Click on teacher: '${teacher.name}' to open People Details Page.") + peopleListPage.selectPerson(teacher) + + Log.d(ASSERTION_TAG, "Assert that People Details Page displays correct: '${teacher.name}'.") + personDetailsPage.assertIsPerson(teacher.name) + + Log.d(STEP_TAG, "Click on the 'Compose' button on People Details Page.") + personDetailsPage.clickCompose() + + Log.d(ASSERTION_TAG, "Assert that the inbox signature: '$signatureText' text is displayed when composing message from People Details FAB.") + inboxComposePage.assertBodyText("\n\n---\nBest Regards\nCanvas Student") + + Log.d(STEP_TAG, "Click on the 'Close' (X) button on the Compose New Message Page.") + inboxComposePage.clickOnCloseButton() + + Log.d(STEP_TAG, "Navigate back to Dashboard.") + pressBackButton(3) + + Log.d(STEP_TAG, "Click on 'Change User' button on the Left Side Navigation Drawer menu.") + leftSideNavigationDrawerPage.clickChangeUserMenu() + + Log.d(STEP_TAG, "Click on the 'Find another school' button.") + loginLandingPage.clickFindAnotherSchoolButton() + + Log.d(STEP_TAG, "Enter domain: '${CanvasNetworkAdapter.canvasDomain}'") + loginFindSchoolPage.enterDomain(CanvasNetworkAdapter.canvasDomain) + + Log.d(STEP_TAG, "Click on 'Next' button on the toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(STEP_TAG, "Login with the other user: '${student2.name}', login id: '${student2.loginId}'.") + loginSignInPage.loginAs(student2) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Select course: '${course.name}'.") + dashboardPage.selectCourse(course) + + Log.d(STEP_TAG, "Navigate to People page.") + courseBrowserPage.selectPeople() + + Log.d(STEP_TAG, "Click on teacher: '${teacher.name}' to open People Details Page.") + peopleListPage.selectPerson(teacher) + + Log.d(ASSERTION_TAG, "Assert that People Details Page displays correct: '${teacher.name}'.") + personDetailsPage.assertIsPerson(teacher.name) + + Log.d(STEP_TAG, "Click on the 'Compose' button on People Details Page.") + personDetailsPage.clickCompose() + + Log.d(ASSERTION_TAG, "Assert that the previously set inbox signature text is NOT displayed since it was set for another user.") + inboxComposePage.assertBodyText("") + + Log.d(STEP_TAG, "Click on the 'Close' (X) button on the Compose New Message Page.") + inboxComposePage.clickOnCloseButton() + + Log.d(STEP_TAG, "Navigate back to Dashboard.") + pressBackButton(3) + + Log.d(STEP_TAG, "Open the Left Side Navigation Drawer menu.") + dashboardPage.openLeftSideMenu() + + Log.d(STEP_TAG, "Navigate to Settings Page on the Left Side Navigation Drawer menu.") + leftSideNavigationDrawerPage.clickSettingsMenu() + + Log.d(ASSERTION_TAG, "Assert that by default the Inbox Signature is 'Not Set'.") + settingsPage.assertSettingsItemDisplayed("Inbox Signature", "Not Set") + + Log.d(STEP_TAG, "Click on the 'Inbox Signature' settings.") + settingsPage.clickOnSettingsItem("Inbox Signature") + + Log.d(ASSERTION_TAG, "Assert that by default the 'Inbox Signature' toggle is turned off.") + inboxSignatureSettingsPage.assertSignatureEnabledState(false) + + val secondSignatureText = "Loyal member of Instructure" + Log.d(STEP_TAG, "Turn on the 'Inbox Signature' and set the inbox signature text to: '$secondSignatureText'. Save the changes.") + inboxSignatureSettingsPage.toggleSignatureEnabledState() + inboxSignatureSettingsPage.changeSignatureText(secondSignatureText) + inboxSignatureSettingsPage.saveChanges() + + Log.d(STEP_TAG, "Refresh the Settings page.") + settingsPage.refresh() + + Log.d(ASSERTION_TAG, "Assert that the Inbox Signature became 'Enabled'.") + settingsPage.assertSettingsItemDisplayed("Inbox Signature", "Enabled") + + Log.d(STEP_TAG, "Navigate back to the Dashboard.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Select course: '${course.name}'.") + dashboardPage.selectCourse(course) + + Log.d(STEP_TAG, "Navigate to People page.") + courseBrowserPage.selectPeople() + + Log.d(STEP_TAG, "Click on teacher: '${teacher.name}' to open People Details Page.") + peopleListPage.selectPerson(teacher) + + Log.d(ASSERTION_TAG, "Assert that People Details Page displays correct: '${teacher.name}'.") + personDetailsPage.assertIsPerson(teacher.name) + + Log.d(STEP_TAG, "Click on the 'Compose' button on People Details Page.") + personDetailsPage.clickCompose() + + Log.d(ASSERTION_TAG, "Assert that the previously set inbox signature: '$secondSignatureText' text is displayed when composing message from People Details page FAB.") + inboxComposePage.assertBodyText("\n\n---\nLoyal member of Instructure") + } + } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/k5/HomeroomE2ETest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/k5/HomeroomE2ETest.kt index 42d3fae041..b729230ee2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/k5/HomeroomE2ETest.kt @@ -14,17 +14,17 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.k5 +package com.instructure.student.ui.e2e.compose.k5 import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.model.SubmissionType @@ -33,10 +33,10 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.page.getStringFromResource import com.instructure.student.R -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedDataForK5 -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.seedDataForK5 +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import org.threeten.bp.OffsetDateTime diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ResourcesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/k5/ResourcesE2ETest.kt similarity index 92% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ResourcesE2ETest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/k5/ResourcesE2ETest.kt index 6f94255daf..4425e1180f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ResourcesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/compose/k5/ResourcesE2ETest.kt @@ -14,21 +14,21 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.k5 +package com.instructure.student.ui.e2e.compose.k5 import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.model.CanvasUserApiModel -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.seedDataForK5 -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.seedDataForK5 +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt index 305e443bdc..3f02c95c28 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt @@ -19,23 +19,23 @@ import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addGroupToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addGroupToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.models.User -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt index 3eee878026..19384887e6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt @@ -25,23 +25,24 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.checkToastText -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addAssignmentsToGroups -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addAssignmentsToGroups +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.model.SubmissionType import com.instructure.pandautils.utils.toFormattedString import com.instructure.student.R import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.routeTo -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.routeTo +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -141,7 +142,7 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { val data = setUpData() goToAssignmentList() val calendar = Calendar.getInstance().apply { set(2023, 0, 31, 23, 59, 0) } - val expectedDueDate = "January 31, 2023 11:59 PM" + val expectedDueDate = "Jan 31, 2023 11:59 PM" val course = data.courses.values.first() val assignmentWithNoDueDate = data.addAssignment(course.id, name = "Test Assignment", dueAt = calendar.time.toApiString()) assignmentListPage.refreshAssignmentList() @@ -150,6 +151,43 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { assignmentDetailsPage.assertDisplaysDate(expectedDueDate) } + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun testDisplayDueDates() { + val data = setUpData() + goToAssignmentList() + + var calendar = Calendar.getInstance().apply { set(2023, 0, 29, 23, 59, 0) } + val expectedReplyToTopicDueDate = "Jan 29, 2023 11:59 PM" + val replyToTopicDueDate = calendar.time.toApiString() + + calendar = Calendar.getInstance().apply { set(2023, 0, 31, 23, 59, 0) } + val expectedReplyToEntryDueDate = "Jan 31, 2023 11:59 PM" + val replyToEntryDueDate = calendar.time.toApiString() + val course = data.courses.values.first() + + val checkpoints = listOf( + Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + dueAt = replyToTopicDueDate, + pointsPossible = 10.0 + ), + Checkpoint( + name = "Reply to Entry", + tag = "reply_to_entry", + dueAt = replyToEntryDueDate, + pointsPossible = 10.0 + ) + ) + val assignmentWithNoDueDate = data.addAssignment(course.id, name = "Test Assignment", dueAt = calendar.time.toApiString(), checkpoints = checkpoints) + assignmentListPage.refreshAssignmentList() + assignmentListPage.clickAssignment(assignmentWithNoDueDate) + + assignmentDetailsPage.assertDisplaysDate(expectedReplyToTopicDueDate, 0) + assignmentDetailsPage.assertDisplaysDate(expectedReplyToEntryDueDate, 1) + } + @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) fun testNavigating_viewAssignmentDetails() { @@ -400,7 +438,39 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { assignmentListPage.clickAssignment(assignment) - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun testReminderSectionsAreVisibleWhenThereAreNoFutureDueDates() { + val data = setUpData() + val course = data.courses.values.first() + + val pastDate = Calendar.getInstance().apply { + add(Calendar.DAY_OF_MONTH, -1) + }.time.toApiString() + + val checkpoints = listOf( + Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + dueAt = pastDate, + pointsPossible = 10.0 + ), + Checkpoint( + name = "Reply to Entry", + tag = "reply_to_entry", + dueAt = pastDate, + pointsPossible = 10.0 + ) + ) + val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = pastDate, checkpoints = checkpoints) + goToAssignmentList() + + assignmentListPage.clickAssignment(assignment) + assignmentDetailsPage.assertReminderViewDisplayed(0) + assignmentDetailsPage.assertReminderViewDisplayed(1) } @Test @@ -413,7 +483,7 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { assignmentListPage.clickAssignment(assignment) - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @Test @@ -428,7 +498,7 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { assignmentListPage.clickAssignment(assignment) - reminderPage.assertReminderSectionDisplayed() + assignmentReminderPage.assertReminderSectionDisplayed() } @Test @@ -445,12 +515,12 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { goToAssignmentList() assignmentListPage.clickAssignment(assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) - reminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) } @Test @@ -467,17 +537,17 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { goToAssignmentList() assignmentListPage.clickAssignment(assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) - reminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.assertReminderDisplayedWithText(reminderCalendar.time.toFormattedString()) - reminderPage.removeReminderWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.removeReminderWithText(reminderCalendar.time.toFormattedString()) - reminderPage.assertReminderNotDisplayedWithText(reminderCalendar.time.toFormattedString()) + assignmentReminderPage.assertReminderNotDisplayedWithText(reminderCalendar.time.toFormattedString()) } @Test @@ -494,10 +564,10 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { goToAssignmentList() assignmentListPage.clickAssignment(assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) checkToastText(R.string.reminderInPast, activityRule.activity) } @@ -517,19 +587,50 @@ class AssignmentDetailsInteractionTest : StudentComposeTest() { assignmentListPage.clickAssignment(assignment) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) - reminderPage.clickAddReminder() - reminderPage.clickCustomReminderOption() - reminderPage.selectDate(reminderCalendar) - reminderPage.selectTime(reminderCalendar) + assignmentReminderPage.clickAddReminder() + assignmentReminderPage.clickCustomReminderOption() + assignmentReminderPage.selectDate(reminderCalendar) + assignmentReminderPage.selectTime(reminderCalendar) checkToastText(R.string.reminderAlreadySet, activityRule.activity) } + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun testDiscussionCheckpointsDisplayed() { + val data = setUpData() + val course = data.courses.values.first() + + val checkpoint1 = Checkpoint( + tag = "reply_to_topic", + pointsPossible = 5.0, + dueAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 1) }.time.toApiString() + ) + val checkpoint2 = Checkpoint( + tag = "reply_to_entry", + pointsPossible = 5.0, + dueAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 2) }.time.toApiString() + ) + + val assignment = data.addAssignment( + courseId = course.id, + name = "Discussion Checkpoint Assignment", + checkpoints = listOf(checkpoint1, checkpoint2), + submissionTypeList = listOf(Assignment.SubmissionType.DISCUSSION_TOPIC) + ) + + goToAssignmentList() + assignmentListPage.clickAssignment(assignment) + + assignmentDetailsPage.assertCheckpointDisplayed(0, "Reply to topic", "-/5") + assignmentDetailsPage.assertCheckpointDisplayed(1, "Additional replies (0)", "-/5") + } + private fun setUpData(restrictQuantitativeData: Boolean = false): MockCanvas { // Test clicking on the Submission and Rubric button to load the Submission Details Page val data = MockCanvas.init( diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt index 6ab549a22a..fe5fb60df1 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt @@ -19,17 +19,17 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CourseSettings import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt index 4de9bd32f9..0d050ae7b9 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt @@ -24,17 +24,17 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addBookmark -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addBookmark +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvas.espresso.refresh import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt index e7ec64c5ad..cd57303fc2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt @@ -21,16 +21,16 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.Grades import com.instructure.canvasapi2.models.Tab import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt index f012d8706d..1f515925b2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt @@ -27,15 +27,15 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addPageToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addPageToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Tab import com.instructure.student.R import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.CoreMatchers.containsString import org.junit.Test @@ -101,7 +101,7 @@ class CourseInteractionTest : StudentTest() { val course = data.courses.values.first() val displayName = "FamousQuote.html" - val fileId = data.addFileToCourse( + data.addFileToCourse( courseId = course.id, displayName = displayName, fileContent = """ @@ -129,21 +129,54 @@ class CourseInteractionTest : StudentTest() { .check(webMatches(getText(), containsString("Ask not"))) } + // Home button should open the front page and display the correct header/title + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.COURSE, TestCategory.INTERACTION) + fun testCourse_homeDisplaysFrontPageTitle() { + + getToCourse(courseCount = 1, favoriteCourseCount = 1) { data, course -> + + data.addPageToCourse( + courseId = course.id, + pageId = 1, + url = "front-page", + title = "Front Page", + body = "

Very interesting Page

", + published = true, + frontPage = true + ) + + data.courses[course.id] = data.courses[course.id]!!.copy(homePage = Course.HomePage.HOME_WIKI) + } + + courseBrowserPage.selectHome() + + onWebView(withId(R.id.contentWebView)) + .withElement(findElement(Locator.ID, "header1")) + .check(webMatches(getText(), containsString("Very interesting Page"))) + } + /** Utility method to create mocked data, sign student 0 in, and navigate to course 0. */ private fun getToCourse( - courseCount: Int = 2, // Need to link from one to the other - favoriteCourseCount: Int = 1): MockCanvas { + courseCount: Int = 2, + favoriteCourseCount: Int = 1, + configure: (MockCanvas, Course) -> Unit = { _, _ -> } + ): MockCanvas { val data = MockCanvas.init( studentCount = 1, courseCount = courseCount, favoriteCourseCount = favoriteCourseCount) val course1 = data.courses.values.first() + val homeTab = Tab(position = 1, label = "Home", visibility = "public", tabId = Tab.HOME_ID) val pagesTab = Tab(position = 2, label = "Pages", visibility = "public", tabId = Tab.PAGES_ID) val filesTab = Tab(position = 3, label = "Files", visibility = "public", tabId = Tab.FILES_ID) + data.courseTabs[course1.id]!! += homeTab data.courseTabs[course1.id]!! += pagesTab data.courseTabs[course1.id]!! += filesTab + configure(data, course1) + val student = data.students[0] val token = data.tokenFor(student)!! tokenLogin(data.domain, token, student) @@ -155,4 +188,3 @@ class CourseInteractionTest : StudentTest() { return data } } - diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt index baf444bf42..cc94803123 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt @@ -22,9 +22,9 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAccountNotification -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAccountNotification +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.Grades @@ -32,7 +32,7 @@ import com.instructure.pandautils.di.NetworkStateProviderModule import com.instructure.pandautils.utils.NetworkStateProvider import com.instructure.student.espresso.fakes.FakeNetworkStateProvider import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt index 9a807b921f..ecdc8a41d4 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt @@ -21,28 +21,31 @@ import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addReplyToDiscussion -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addReplyToDiscussion +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.DiscussionEntry import com.instructure.canvasapi2.models.RemoteFile import com.instructure.canvasapi2.models.Tab -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.canvasapi2.utils.toApiString +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Assert.assertNotNull import org.junit.Test +import java.util.Calendar // Note: Tests course discussions, not group discussions. @HiltAndroidTest @@ -721,6 +724,90 @@ class DiscussionsInteractionTest : StudentTest() { return data } + // Tests that checkpoint due dates are displayed in the discussion list + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + fun testDiscussionList_displayCheckpointDates() { + val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) + val course = data.courses.values.first() + val teacher = data.users.values.first() + + val calendar1 = Calendar.getInstance().apply { set(2023, 0, 29, 13, 30, 0) } + val expectedDate1 = "Due Jan 29, 2023 1:30 PM" + val checkpoint1DueDate = calendar1.time.toApiString() + + val calendar2 = Calendar.getInstance().apply { set(2023, 0, 31, 15, 30, 0) } + val expectedDate2 = "Due Jan 31, 2023 3:30 PM" + val checkpoint2DueDate = calendar2.time.toApiString() + + val checkpoints = listOf( + Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + dueAt = checkpoint1DueDate, + pointsPossible = 10.0 + ), + Checkpoint( + name = "Reply to Entry", + tag = "reply_to_entry", + dueAt = checkpoint2DueDate, + pointsPossible = 10.0 + ) + ) + + val assignment = data.addAssignment( + courseId = course.id, + submissionTypeList = listOf(Assignment.SubmissionType.DISCUSSION_TOPIC), + name = "Discussion with Checkpoints", + pointsPossible = 20, + checkpoints = checkpoints + ) + + data.addDiscussionTopicToCourse( + course = course, + user = teacher, + topicTitle = "Discussion with Checkpoints", + topicDescription = "Test checkpoints display", + assignment = assignment + ) + + courseBrowserPage.selectDiscussions() + discussionListPage.pullToUpdate() + discussionListPage.assertTopicDisplayed("Discussion with Checkpoints") + discussionListPage.assertCheckpointDueDates("Discussion with Checkpoints", "$expectedDate1\n$expectedDate2") + } + + // Tests that points possible are displayed in the discussion list + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + fun testDiscussionList_displayPointsPossible() { + val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) + val course = data.courses.values.first() + val teacher = data.users.values.first() + + val assignment = data.addAssignment( + courseId = course.id, + submissionTypeList = listOf(Assignment.SubmissionType.DISCUSSION_TOPIC), + name = "Discussion with Points", + pointsPossible = 15 + ) + + data.addDiscussionTopicToCourse( + course = course, + user = teacher, + topicTitle = "Discussion with Points", + topicDescription = "Test points display", + assignment = assignment + ) + + courseBrowserPage.selectDiscussions() + discussionListPage.pullToUpdate() + discussionListPage.assertTopicDisplayed("Discussion with Points") + + // Verify points are displayed in readUnreadCounts (alongside reply counts) + discussionListPage.assertPointsDisplayed("Discussion with Points", 15) + } + companion object { // Creates an HTML attachment/file which can then be attached to a topic header or reply. fun createHtmlAttachment(data: MockCanvas, html: String): RemoteFile { @@ -743,7 +830,5 @@ class DiscussionsInteractionTest : StudentTest() { return attachment } - } - } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt index c77a537379..e7cec296db 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt @@ -20,11 +20,11 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryGradesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryGradesInteractionTest.kt index bad0d87003..7a9703597d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryGradesInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryGradesInteractionTest.kt @@ -21,15 +21,15 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCourseWithEnrollment -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCourseWithEnrollment +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Enrollment import com.instructure.espresso.page.getStringFromResource import com.instructure.student.R -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt index 16579a02bd..4f5545bb7f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt @@ -23,22 +23,22 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addFileToFolder -import com.instructure.canvas.espresso.mockCanvas.addFolderToCourse -import com.instructure.canvas.espresso.mockCanvas.addGroupToCourse -import com.instructure.canvas.espresso.mockCanvas.addPageToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addFileToFolder +import com.instructure.canvas.espresso.mockcanvas.addFolderToCourse +import com.instructure.canvas.espresso.mockcanvas.addGroupToCourse +import com.instructure.canvas.espresso.mockcanvas.addPageToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvas.espresso.refresh import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.models.Page import com.instructure.canvasapi2.models.Tab -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt index 3566665576..0b90b7393d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt @@ -20,21 +20,21 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCourseWithEnrollment -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCourseWithEnrollment +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Enrollment import com.instructure.espresso.page.getStringFromResource import com.instructure.student.R -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt index 77afadd9d0..70a6f702dd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt @@ -18,15 +18,15 @@ package com.instructure.student.ui.interaction import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.StubTablet import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addAssignmentCalendarEvent -import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.StubTablet +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addAssignmentCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.addCourseCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -34,9 +34,9 @@ import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt index 489c813e44..d2a00b7d67 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt @@ -23,17 +23,15 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.google.android.play.core.appupdate.testing.FakeAppUpdateManager -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.StubTablet -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.utils.toApiString import com.instructure.pandautils.di.UpdateModule import com.instructure.pandautils.update.UpdateManager import com.instructure.pandautils.update.UpdatePrefs import com.instructure.student.R import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt index 9310441190..ce42ba33cf 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.platform.ComposeView import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.web.webdriver.Locator +import androidx.test.platform.app.InstrumentationRegistry import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.FeatureCategory @@ -27,20 +28,24 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addItemToModule -import com.instructure.canvas.espresso.mockCanvas.addLTITool -import com.instructure.canvas.espresso.mockCanvas.addModuleToCourse -import com.instructure.canvas.espresso.mockCanvas.addPageToCourse -import com.instructure.canvas.espresso.mockCanvas.addQuestionToQuiz -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addItemToModule +import com.instructure.canvas.espresso.mockcanvas.addLTITool +import com.instructure.canvas.espresso.mockcanvas.addModuleToCourse +import com.instructure.canvas.espresso.mockcanvas.addPageToCourse +import com.instructure.canvas.espresso.mockcanvas.addQuestionToQuiz +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeModuleManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule +import com.instructure.canvasapi2.di.graphql.ModuleManagerModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager +import com.instructure.canvasapi2.managers.graphql.ModuleItemCheckpoint +import com.instructure.canvasapi2.managers.graphql.ModuleManager import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.DiscussionTopicHeader @@ -52,28 +57,34 @@ import com.instructure.canvasapi2.models.Page import com.instructure.canvasapi2.models.Quiz import com.instructure.canvasapi2.models.QuizAnswer import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.DateHelper import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.R -import com.instructure.student.ui.pages.WebViewTextCheck -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.pages.classic.WebViewTextCheck +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules import org.hamcrest.Matchers import org.junit.Test import java.net.URLEncoder +import java.util.Date @HiltAndroidTest -@UninstallModules(CustomGradeStatusModule::class) -class ModuleInteractionTest : StudentTest() { +@UninstallModules(CustomGradeStatusModule::class, ModuleManagerModule::class) +class ModuleInteractionTest : StudentComposeTest() { @BindValue @JvmField val customGradeStatusesManager: CustomGradeStatusesManager = FakeCustomGradeStatusesManager() + @BindValue + @JvmField + val moduleManager: ModuleManager = FakeModuleManager() + override fun displaysPageObjects() = Unit // Not used for interaction tests // A collection of things that we create during initialization and remember for use during @@ -625,6 +636,100 @@ class ModuleInteractionTest : StudentTest() { modulesPage.assertPossiblePointsNotDisplayed(assignment.name.orEmpty()) } + // Discussion checkpoint dates should be displayed in module list + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.MODULES, TestCategory.INTERACTION, SecondaryFeatureCategory.MODULES_DISCUSSIONS) + fun testModules_discussionCheckpointDatesDisplayed() { + // Set up all mock data BEFORE navigating to the page + val data = MockCanvas.init( + studentCount = 1, + courseCount = 1, + favoriteCourseCount = 1 + ) + + val course = data.courses.values.first() + val user = data.users.values.first() + + // Add a course tab + val modulesTab = Tab(position = 2, label = "Modules", visibility = "public", tabId = Tab.MODULES_ID) + data.courseTabs[course.id]!! += modulesTab + + // Create a module + data.addModuleToCourse( + course = course, + moduleName = "Big Module" + ) + + val module = data.courseModules[course.id]!!.first() + + // Create a discussion and add it as a module item + val discussionTitle = "Discussion with Checkpoints" + topicHeader = data.addDiscussionTopicToCourse( + course = course, + user = user, + topicTitle = discussionTitle, + topicDescription = "A discussion with checkpoints" + ) + data.addItemToModule( + course = course, + moduleId = module.id, + item = topicHeader!!, + moduleContentDetails = ModuleContentDetails() + ) + + // Get the module item ID from the module items + val updatedModule = data.courseModules[course.id]!!.first() + val discussionModuleItem = updatedModule.items.find { it.title == discussionTitle }!! + + // Set up fake checkpoint data BEFORE navigating to the page + val fakeModuleManager = moduleManager as FakeModuleManager + + val checkpointDate1 = Date(1750089600000L) // Jun 16, 2025 6:00 PM + val checkpointDate2 = Date(1750608000000L) // Jun 22, 2025 6:00 PM + + fakeModuleManager.setCheckpoints( + courseId = course.id.toString(), + moduleItemId = discussionModuleItem.id.toString(), + checkpoints = listOf( + ModuleItemCheckpoint( + dueAt = checkpointDate1, + tag = "reply_to_topic", + pointsPossible = 5.0 + ), + ModuleItemCheckpoint( + dueAt = checkpointDate2, + tag = "reply_to_entry", + pointsPossible = 5.0 + ) + ) + ) + + // NOW navigate to the modules page with the checkpoint data already set up + val student = data.students[0] + val token = data.tokenFor(student)!! + tokenLogin(data.domain, token, student) + dashboardPage.waitForRender() + + // Navigate to the course + dashboardPage.selectCourse(course) + // Navigate to the modules page - this will trigger the initial load with checkpoints + courseBrowserPage.selectModules() + + val expectedDateText1 = DateHelper.createPrefixedDateTimeString( + InstrumentationRegistry.getInstrumentation().targetContext, + R.string.toDoDue, + checkpointDate1 + ) + val expectedDateText2 = DateHelper.createPrefixedDateTimeString( + InstrumentationRegistry.getInstrumentation().targetContext, + R.string.toDoDue, + checkpointDate2 + ) + + // Assert that checkpoint dates are displayed + modulesPage.assertCheckpointDatesDisplayed(discussionTitle, listOf(expectedDateText1!!, expectedDateText2!!)) + } + // Mock a specified number of students and courses, add some assorted assignments, discussions, etc... // in the form of module items, and navigate to the modules page of the course private fun getToCourseModules( diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt index f0d9aac9f9..11e6687383 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt @@ -26,8 +26,8 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.ApiPrefs @@ -39,8 +39,8 @@ import com.instructure.pandautils.utils.NetworkStateProvider import com.instructure.student.R import com.instructure.student.espresso.fakes.FakeNetworkStateProvider import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -146,7 +146,7 @@ class NavigationDrawerInteractionTest : StudentTest() { signInStudent() leftSideNavigationDrawerPage.clickHelpMenu() - helpPage.verifyAskAQuestion(course, "Here's a question") + helpPage.assertAskYourInstructorDialogDetails(course, "Here's a question") } // Should open the Canvas guides in a WebView @@ -169,7 +169,7 @@ class NavigationDrawerInteractionTest : StudentTest() { signInStudent() leftSideNavigationDrawerPage.clickHelpMenu() - helpPage.verifyReportAProblem("Problem", "It's a problem!") + helpPage.assertReportProblemDialogDetails("Problem", "It's a problem!") } // Should send an intent to open the listing for Student App in the Play Store diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt index 71c6db7740..c853774a19 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt @@ -19,21 +19,24 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.addSubmissionStreamItem -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.addSubmissionStreamItem +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.dataseeding.util.ago import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.pandautils.utils.Const +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -42,7 +45,7 @@ import java.util.UUID @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class NotificationInteractionTest : StudentTest() { +class NotificationInteractionTest : StudentComposeTest() { @BindValue @JvmField @@ -238,13 +241,36 @@ class NotificationInteractionTest : StudentTest() { notificationPage.assertExcused(assignment.name!!) } + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION) + fun testNotificationList_showCheckpointLabelForReplyToTopic() { + val data = goToNotifications(checkpointTag = Const.REPLY_TO_TOPIC) + + val assignment = data.assignments.values.first() + + notificationPage.assertCheckpointLabel(assignment.name!!, "Reply to topic") + } + + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION) + fun testNotificationList_showCheckpointLabelForAdditionalReplies() { + val replyCount = 3 + val data = goToNotifications(checkpointTag = Const.REPLY_TO_ENTRY, replyRequiredCount = replyCount) + + val assignment = data.assignments.values.first() + + notificationPage.assertCheckpointLabel(assignment.name!!, "Additional replies ($replyCount)") + } + private fun goToNotifications( numSubmissions: Int = 1, restrictQuantitativeData: Boolean = false, gradingType: Assignment.GradingType = Assignment.GradingType.POINTS, score: Double = -1.0, grade: String? = null, - excused: Boolean = false + excused: Boolean = false, + checkpointTag: String? = null, + replyRequiredCount: Int = 0 ): MockCanvas { val data = MockCanvas.init(courseCount = 1, favoriteCourseCount = 1, studentCount = 1, teacherCount = 1) @@ -264,24 +290,63 @@ class NotificationInteractionTest : StudentTest() { gradingSchemeRaw = gradingScheme) repeat(numSubmissions) { + val isCheckpoint = checkpointTag != null + + val checkpoints = if (isCheckpoint) { + listOf( + Checkpoint( + name = if (checkpointTag == Const.REPLY_TO_TOPIC) "Reply to topic" else "Additional replies", + tag = checkpointTag, + pointsPossible = 10.0, + dueAt = 1.days.ago.iso8601, + overrides = emptyList() + ) + ) + } else { + emptyList() + } + + val submissionType = if (isCheckpoint) { + Assignment.SubmissionType.DISCUSSION_TOPIC + } else { + Assignment.SubmissionType.ONLINE_TEXT_ENTRY + } + val assignment = data.addAssignment( courseId = course.id, - submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY), + submissionTypeList = listOf(submissionType), gradingType = Assignment.gradingTypeToAPIString(gradingType).orEmpty(), - pointsPossible = 20 + pointsPossible = 20, + checkpoints = checkpoints ) + val finalAssignment = if (isCheckpoint) { + assignment.copy( + subAssignmentTag = checkpointTag, + discussionTopicHeader = DiscussionTopicHeader( + id = assignment.id, + replyRequiredCount = replyRequiredCount + ) + ) + } else { + assignment + } + + if (isCheckpoint) { + data.assignments[assignment.id] = finalAssignment + } + val submission = data.addSubmissionForAssignment( assignmentId = assignment.id, userId = student.id, - type = Assignment.SubmissionType.ONLINE_TEXT_ENTRY.apiString, - body = "Some words + ${UUID.randomUUID()}" + type = submissionType.apiString, + body = if (isCheckpoint) "Discussion checkpoint submission" else "Some words + ${UUID.randomUUID()}" ) data.addSubmissionStreamItem( user = student, course = course, - assignment = assignment, + assignment = finalAssignment, submission = submission, submittedAt = 1.days.ago.iso8601, type = "submission", diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt index cd06987a98..3e5804dcc1 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt @@ -28,17 +28,16 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Tab import com.instructure.dataseeding.util.Randomizer import com.instructure.pandautils.R import com.instructure.pandautils.utils.StorageUtils import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt index 6c8da16aa8..75c0eeb1b5 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt @@ -30,14 +30,14 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAnnotation -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addAssignmentsToGroups -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAnnotation +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addAssignmentsToGroups +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -47,8 +47,8 @@ import com.instructure.pandautils.loaders.OpenMediaAsyncTaskLoader import com.instructure.pandautils.utils.filecache.FileCache import com.instructure.student.R import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.routeTo -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.routeTo +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt index d3339ff86d..ae12cfd141 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt @@ -19,12 +19,12 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.routeTo -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.routeTo +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt index 05b8c5b8c0..0620ff5dfa 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt @@ -22,7 +22,6 @@ import android.content.Intent import android.net.Uri import android.provider.MediaStore import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.test.espresso.Espresso import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intending @@ -37,20 +36,19 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment import com.instructure.pandautils.utils.FilePrefs -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -64,7 +62,7 @@ import java.io.File @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class PickerSubmissionUploadInteractionTest : StudentTest() { +class PickerSubmissionUploadInteractionTest : StudentComposeTest() { @BindValue @JvmField @@ -76,11 +74,6 @@ class PickerSubmissionUploadInteractionTest : StudentTest() { private lateinit var activity : Activity private lateinit var activityResult: Instrumentation.ActivityResult - @get:Rule - val composeTestRule = createEmptyComposeRule() - - val assignmentListPage by lazy { AssignmentListPage(composeTestRule) } - @Before fun setUp() { // Read this at set-up, because it may become null soon thereafter @@ -197,37 +190,6 @@ class PickerSubmissionUploadInteractionTest : StudentTest() { pickerSubmissionUploadPage.assertFileDisplayed(mockedFileName) } - @Test - @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) - fun testFab_scanner(){ - val scannerComponent = "com.instructure.student.features.documentscanning.DocumentScanningActivity" - - goToSubmissionPicker() - - Intents.init() - try { - val context = getInstrumentation().targetContext - val dir = context.externalCacheDir - val sampleFile = File(dir, mockedFileName) - val uri = Uri.fromFile(sampleFile) - val resultData = Intent().apply { data = uri } - val scannerResult = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData) - - intending( - IntentMatchers.hasComponent(scannerComponent) - ).respondWith(scannerResult) - - pickerSubmissionUploadPage.chooseScanner() - } finally { - release() - } - - pickerSubmissionUploadPage.waitForSubmitButtonToAppear() - - pickerSubmissionUploadPage.assertFileDisplayed(mockedFileName) - - } - @Test @TestMetaData(Priority.COMMON, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) fun testDeleteFile() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt index b2e05bcc1a..c459c4cffa 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt @@ -10,12 +10,12 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addUserPermissions -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addUserPermissions +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.student.R import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.junit.Assert.assertTrue diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/QuizListInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/QuizListInteractionTest.kt index cd90ead0f5..3ca5edd7b0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/QuizListInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/QuizListInteractionTest.kt @@ -19,13 +19,13 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.Quiz import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt index 1f4ed289cb..a31a7fe86a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt @@ -20,15 +20,15 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCourseWithEnrollment -import com.instructure.canvas.espresso.mockCanvas.addEnrollment -import com.instructure.canvas.espresso.mockCanvas.addLTITool -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCourseWithEnrollment +import com.instructure.canvas.espresso.mockcanvas.addEnrollment +import com.instructure.canvas.espresso.mockcanvas.addLTITool +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Enrollment -import com.instructure.student.ui.pages.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt index da968141ff..bcddb0dca6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt @@ -18,15 +18,15 @@ package com.instructure.student.ui.interaction import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.StubLandscape -import com.instructure.canvas.espresso.StubTablet import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addTodo -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.StubLandscape +import com.instructure.canvas.espresso.annotations.StubTablet +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addTodo +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -34,10 +34,10 @@ import com.instructure.canvasapi2.utils.toApiString import com.instructure.espresso.page.getStringFromResource import com.instructure.pandautils.utils.date.DateTimeProvider import com.instructure.student.R -import com.instructure.student.ui.pages.ElementaryDashboardPage -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.di.FakeDateTimeProvider -import com.instructure.student.ui.utils.tokenLoginElementary +import com.instructure.student.ui.utils.extensions.tokenLoginElementary import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -48,7 +48,7 @@ import javax.inject.Inject @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class ScheduleInteractionTest : StudentTest() { +class ScheduleInteractionTest : StudentComposeTest() { @BindValue @JvmField diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt index 6a127b441c..daee0a97c7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt @@ -18,15 +18,15 @@ package com.instructure.student.ui.interaction import android.app.Activity import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.StubMultiAPILevel import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.StubMultiAPILevel +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt index 09d766e2be..b8e1af92f1 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt @@ -23,16 +23,16 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.StubCoverage -import com.instructure.canvas.espresso.StubTablet -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.annotations.StubCoverage +import com.instructure.canvas.espresso.annotations.StubTablet +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.User import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.io.File diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt index a99ffde9bc..bbbea2b0ff 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt @@ -17,9 +17,9 @@ package com.instructure.student.ui.interaction import com.instructure.canvas.espresso.common.interaction.CalendarInteractionTest import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.User @@ -27,10 +27,10 @@ import com.instructure.espresso.ModuleItemInteractions import com.instructure.student.BuildConfig import com.instructure.student.R import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage -import com.instructure.student.ui.pages.DiscussionDetailsPage +import com.instructure.student.ui.pages.classic.DashboardPage +import com.instructure.student.ui.pages.classic.DiscussionDetailsPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -48,7 +48,7 @@ class StudentCalendarInteractionTest : CalendarInteractionTest() { override val activityRule = StudentActivityTestRule(LoginActivity::class.java) private val dashboardPage = DashboardPage() - private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) + private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(), composeTestRule) private val discussionDetailsPage = DiscussionDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item)) override fun goToCalendar(data: MockCanvas) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateEventInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateEventInteractionTest.kt index 31ae49008e..d8f4b8d97c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateEventInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateEventInteractionTest.kt @@ -16,14 +16,14 @@ package com.instructure.student.ui.interaction import com.instructure.canvas.espresso.common.interaction.CreateUpdateEventInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage +import com.instructure.student.ui.pages.classic.DashboardPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateToDoInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateToDoInteractionTest.kt index 93d37f50ac..3f30c41fa3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateToDoInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCreateUpdateToDoInteractionTest.kt @@ -16,14 +16,14 @@ package com.instructure.student.ui.interaction import com.instructure.canvas.espresso.common.interaction.CreateUpdateToDoInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage +import com.instructure.student.ui.pages.classic.DashboardPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentEventDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentEventDetailsInteractionTest.kt index 41e571f398..f13bc9fdd1 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentEventDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentEventDetailsInteractionTest.kt @@ -16,13 +16,13 @@ package com.instructure.student.ui.interaction import com.instructure.canvas.espresso.common.interaction.EventDetailsInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage +import com.instructure.student.ui.pages.classic.DashboardPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxComposeInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxComposeInteractionTest.kt index 45030590eb..2fa1ef1776 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxComposeInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxComposeInteractionTest.kt @@ -22,19 +22,19 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.InboxComposeInteractionTest import com.instructure.canvas.espresso.common.pages.InboxPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addRecipientsToCourse -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addRecipientsToCourse +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -51,9 +51,9 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.User import com.instructure.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage +import com.instructure.student.ui.pages.classic.DashboardPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxListInteractionsTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxListInteractionsTest.kt index 33f3ca7445..ce509396f5 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxListInteractionsTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxListInteractionsTest.kt @@ -16,17 +16,17 @@ package com.instructure.student.ui.interaction import com.instructure.canvas.espresso.common.interaction.InboxListInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addRecipientsToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addRecipientsToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.User import com.instructure.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage +import com.instructure.student.ui.pages.classic.DashboardPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxSignatureInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxSignatureInteractionTest.kt index 5e1480cb59..e17a283289 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxSignatureInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxSignatureInteractionTest.kt @@ -20,17 +20,17 @@ import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.InboxSignatureInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -43,9 +43,9 @@ import com.instructure.canvasapi2.managers.graphql.SubmissionDetailsManager import com.instructure.canvasapi2.managers.graphql.SubmissionGradeManager import com.instructure.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.LeftSideNavigationDrawerPage +import com.instructure.student.ui.pages.classic.LeftSideNavigationDrawerPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentSmartSearchInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentSmartSearchInteractionTest.kt index 901e4580d6..ffc1da8c8f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentSmartSearchInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentSmartSearchInteractionTest.kt @@ -21,9 +21,9 @@ import androidx.compose.ui.test.performImeAction import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.requestFocus import com.instructure.canvas.espresso.common.interaction.SmartSearchInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.DiscussionTopicHeader @@ -32,11 +32,11 @@ import com.instructure.canvasapi2.models.Tab import com.instructure.espresso.ModuleItemInteractions import com.instructure.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage -import com.instructure.student.ui.pages.DiscussionDetailsPage -import com.instructure.student.ui.pages.PageDetailsPage +import com.instructure.student.ui.pages.classic.DashboardPage +import com.instructure.student.ui.pages.classic.DiscussionDetailsPage +import com.instructure.student.ui.pages.classic.PageDetailsPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentToDoDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentToDoDetailsInteractionTest.kt index d666ea9436..85ff46ecdd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentToDoDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentToDoDetailsInteractionTest.kt @@ -17,15 +17,15 @@ package com.instructure.student.ui.interaction import android.app.Activity import com.instructure.canvas.espresso.common.interaction.ToDoDetailsInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.espresso.InstructureActivityTestRule import com.instructure.student.BuildConfig import com.instructure.student.activity.LoginActivity -import com.instructure.student.ui.pages.DashboardPage +import com.instructure.student.ui.pages.classic.DashboardPage import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt index 926865ae97..e06b5fa056 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt @@ -17,25 +17,22 @@ package com.instructure.student.ui.interaction import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.test.junit4.createEmptyComposeRule -import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.web.webdriver.Locator import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addRubricToAssignment -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addRubricToAssignment +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -45,20 +42,20 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.RubricCriterion import com.instructure.canvasapi2.models.RubricCriterionRating import com.instructure.canvasapi2.models.SubmissionComment -import com.instructure.student.ui.pages.WebViewTextCheck -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.espresso.handleWorkManagerTask +import com.instructure.student.ui.pages.classic.WebViewTextCheck +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules import org.hamcrest.Matchers -import org.junit.Rule import org.junit.Test import java.util.Date @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class SubmissionDetailsInteractionTest : StudentTest() { +class SubmissionDetailsInteractionTest : StudentComposeTest() { @BindValue @JvmField @@ -68,11 +65,6 @@ class SubmissionDetailsInteractionTest : StudentTest() { private lateinit var course: Course - @get:Rule - val composeTestRule = createEmptyComposeRule() - - val assignmentListPage by lazy { AssignmentListPage(composeTestRule) } - // Should be able to add a comment on a submission @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SUBMISSIONS, TestCategory.INTERACTION) @@ -88,11 +80,14 @@ class SubmissionDetailsInteractionTest : StudentTest() { assignmentListPage.clickAssignment(assignment) assignmentDetailsPage.clickSubmit() urlSubmissionUploadPage.submitText("https://google.com") - Espresso.onIdle() + handleWorkManagerTask("SubmissionWorker") + assignmentDetailsPage.assertAssignmentSubmitted() assignmentDetailsPage.goToSubmissionDetails() submissionDetailsPage.openComments() submissionDetailsPage.addAndSendComment("Hey!") + handleWorkManagerTask("SubmissionWorker") + submissionDetailsPage.assertCommentDisplayed("Hey!", data.users.values.first()) } @@ -110,11 +105,14 @@ class SubmissionDetailsInteractionTest : StudentTest() { assignmentListPage.clickAssignment(assignment) assignmentDetailsPage.clickSubmit() urlSubmissionUploadPage.submitText("https://google.com") + handleWorkManagerTask("SubmissionWorker") + assignmentDetailsPage.assertAssignmentSubmitted() assignmentDetailsPage.assertNoAttemptSpinner() assignmentDetailsPage.clickSubmit() urlSubmissionUploadPage.submitText("https://google.com") + handleWorkManagerTask("SubmissionWorker") assignmentDetailsPage.goToSubmissionDetails() @@ -122,6 +120,8 @@ class SubmissionDetailsInteractionTest : StudentTest() { submissionDetailsPage.assertSelectedAttempt("Attempt 1") submissionDetailsPage.openComments() submissionDetailsPage.addAndSendComment("Hey!") + handleWorkManagerTask("SubmissionWorker") + submissionDetailsPage.assertCommentDisplayed("Hey!", data.users.values.first()) submissionDetailsPage.selectAttempt("Attempt 2") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt index 6d1b6792db..5f8e651a7c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt @@ -19,23 +19,24 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent -import com.instructure.canvas.espresso.mockCanvas.addCourseSettings -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCourseCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.addCourseSettings +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.Tab import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -84,7 +85,22 @@ class SyllabusInteractionTest : StudentComposeTest() { assignmentDetailsPage.assertAssignmentTitle(assignment.name!!) } - private fun goToSyllabus(eventCount: Int, assignmentCount: Int) : MockCanvas { + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.SYLLABUS, TestCategory.INTERACTION) + fun testSyllabus_subAssignment() { + val data = goToSyllabus(eventCount = 0, assignmentCount = 1, withCheckpoints = true) + + val course = data.courses.values.first() + data.coursePermissions[course.id] = CanvasContextPermission(manageCalendar = true) + val assignment = data.assignments.entries.firstOrNull()!!.value + + syllabusPage.selectSummaryTab() + syllabusPage.assertItemDisplayed(assignment.name!!) + syllabusPage.selectSummaryEvent(assignment.name!!) + assignmentDetailsPage.assertAssignmentTitle(assignment.name!!) + } + + private fun goToSyllabus(eventCount: Int, assignmentCount: Int, withCheckpoints: Boolean = false) : MockCanvas { val data = MockCanvas.init(studentCount = 1, teacherCount = 1, courseCount = 1, favoriteCourseCount = 1) @@ -112,10 +128,24 @@ class SyllabusInteractionTest : StudentComposeTest() { } repeat(assignmentCount) { + val checkpoints = if (withCheckpoints) { + listOf( + Checkpoint( + name = "Checkpoint 1", + tag = "checkpoint_1", + pointsPossible = 10.0, + dueAt = 2.days.fromNow.iso8601 + ) + ) + } else { + emptyList() + } + data.addAssignment( courseId = course.id, submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY), - dueAt = 2.days.fromNow.iso8601 + dueAt = 2.days.fromNow.iso8601, + checkpoints = checkpoints ) } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt index 2235fcab13..08975f7866 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt @@ -17,16 +17,15 @@ package com.instructure.student.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.pandautils.R import com.instructure.student.ui.utils.StudentComposeTest -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt index d7cf229736..914f24e310 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt @@ -22,15 +22,15 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.StubLandscape -import com.instructure.canvas.espresso.StubMultiAPILevel import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.StubLandscape +import com.instructure.canvas.espresso.annotations.StubMultiAPILevel +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager import com.instructure.canvasapi2.models.Assignment @@ -39,8 +39,8 @@ import com.instructure.canvasapi2.models.Quiz import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -49,7 +49,7 @@ import org.junit.Test @HiltAndroidTest @UninstallModules(CustomGradeStatusModule::class) -class TodoInteractionTest : StudentTest() { +class TodoInteractionTest : StudentComposeTest() { @BindValue @JvmField diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt index 56d7a4ec4e..aba68bc973 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt @@ -30,15 +30,15 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.StubCoverage import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.annotations.StubCoverage +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.pandautils.utils.Const import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.tokenLogin +import com.instructure.student.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.core.AllOf.allOf import org.junit.Before diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AllCoursesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AllCoursesPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/AllCoursesPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AllCoursesPage.kt index 193f5cfd8d..25071d31dc 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AllCoursesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AllCoursesPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import androidx.appcompat.widget.AppCompatImageView diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnotationCommentListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnotationCommentListPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnotationCommentListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnotationCommentListPage.kt index af8e4ec2e3..7ea7db0296 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnotationCommentListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnotationCommentListPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers import com.instructure.canvas.espresso.scrollRecyclerView diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnouncementListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnouncementListPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnouncementListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnouncementListPage.kt index 26c6b09593..43b34e1f4e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnouncementListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/AnnouncementListPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import com.instructure.espresso.Searchable diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/BookmarkPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/BookmarkPage.kt similarity index 88% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/BookmarkPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/BookmarkPage.kt index 8fa44a4c12..15910d5ef9 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/BookmarkPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/BookmarkPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.appcompat.widget.AppCompatButton import androidx.test.espresso.Espresso @@ -24,6 +24,8 @@ import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiSelector import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.canvas.espresso.scrollRecyclerView @@ -85,4 +87,10 @@ class BookmarkPage : BasePage() { onView(allOf(withId(R.id.title), withText("Delete"), isDisplayed())).click() waitForView(anyOf(withText(android.R.string.ok), withText(R.string.ok))).click() } + + fun addBookmarkToHomeScreen(bookmarkName: String, device: UiDevice) { + clickOnMoreMenu(bookmarkName) + onView(allOf(withId(R.id.title), containsTextCaseInsensitive("Add to Home"), isDisplayed())).click() + device.findObject(UiSelector().textContains("Add automatically")).click() + } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CanvasWebViewPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CanvasWebViewPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/CanvasWebViewPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CanvasWebViewPage.kt index cc2a97e5eb..f02cd80651 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CanvasWebViewPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CanvasWebViewPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.annotation.StringRes import androidx.test.espresso.assertion.ViewAssertions.doesNotExist diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CollaborationsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CollaborationsPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/CollaborationsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CollaborationsPage.kt index 4d40a43b7d..f5b1f8beea 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CollaborationsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CollaborationsPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferenceDetailsPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceDetailsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferenceDetailsPage.kt index e18d491a3c..0a68fd921a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferenceDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.hasSibling import com.instructure.espresso.assertDisplayed diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferenceListPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferenceListPage.kt index 2d7c19a8f8..1384001a0e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferenceListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.matcher.ViewMatchers.hasSibling diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferencesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferencesPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferencesPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferencesPage.kt index 8053db226c..f105b7fcbe 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferencesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ConferencesPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches @@ -111,7 +111,7 @@ object ConferencesPage { /** Assert that a conference with the specified name/title is displayed on the screen. */ fun assertConferenceTitlePresent(title: String) { Web.onWebView(Matchers.allOf(ViewMatchers.withId(R.id.contentWebView), ViewMatchers.isDisplayed())) - .withElement(transform(findConferenceTitleAtom(title), {evaluation -> + .withElement(transform(findConferenceTitleAtom(title), { evaluation -> evaluation.value as ElementReference})) .perform(webScrollIntoView()) .check(webMatches(getText(), containsString(title))) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseBrowserPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseBrowserPage.kt index 9990eea67a..a5a9fbf537 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseBrowserPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import android.widget.LinearLayout diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseGradesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseGradesPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseGradesPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseGradesPage.kt index dd17e17206..d3188db641 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseGradesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/CourseGradesPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.os.SystemClock.sleep import android.view.View diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DashboardPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DashboardPage.kt index a05b8431d4..de89ebf3fd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DashboardPage.kt @@ -16,7 +16,7 @@ */ @file:Suppress("unused") -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import androidx.appcompat.widget.SwitchCompat @@ -38,6 +38,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry import com.instructure.canvas.espresso.scrollRecyclerView +import com.instructure.canvas.espresso.waitForViewToDisappear import com.instructure.canvas.espresso.withCustomConstraints import com.instructure.canvasapi2.models.AccountNotification import com.instructure.canvasapi2.models.Course @@ -73,7 +74,6 @@ import com.instructure.espresso.scrollTo import com.instructure.espresso.swipeDown import com.instructure.espresso.waitForCheck import com.instructure.student.R -import com.instructure.student.ui.utils.ViewUtils import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.containsString @@ -219,6 +219,11 @@ class DashboardPage : BasePage(R.id.dashboardPage) { onView(withId(R.id.titleTextView) + withText(course.originalName)).perform(withCustomConstraints(click(), isDisplayingAtLeast(10))) } + fun selectCourse(courseName: String) { + assertDisplaysCourse(courseName) + onView(withId(R.id.titleTextView) + withText(courseName)).perform(withCustomConstraints(click(), isDisplayingAtLeast(10))) + } + fun selectGroup(group: Group) { val groupNameMatcher = allOf(withText(group.name), withId(R.id.groupNameView)) waitForView(groupNameMatcher).scrollTo().click() @@ -440,7 +445,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) { //OfflineMethod fun waitForSyncProgressDownloadStartedNotificationToDisappear() { - ViewUtils.waitForViewToDisappear(withText(com.instructure.pandautils.R.string.syncProgress_downloadStarting), 30) + waitForViewToDisappear(withText(com.instructure.pandautils.R.string.syncProgress_downloadStarting), 30) } //OfflineMethod @@ -455,7 +460,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) { //OfflineMethod fun waitForSyncProgressStartingNotificationToDisappear() { - ViewUtils.waitForViewToDisappear(withText(com.instructure.pandautils.R.string.syncProgress_syncingOfflineContent), 30) + waitForViewToDisappear(withText(com.instructure.pandautils.R.string.syncProgress_syncingOfflineContent), 30) } //OfflineMethod diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionDetailsPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionDetailsPage.kt index db61d656e4..8d25de1dd7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.hasSibling import androidx.test.espresso.web.assertion.WebViewAssertions.webContent diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionListPage.kt similarity index 90% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionListPage.kt index 6d66e65872..159d39ea88 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/DiscussionListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.hasSibling @@ -182,4 +182,22 @@ open class DiscussionListPage(val searchable: Searchable) : BasePage(R.id.discus val matcher = allOf(withId(R.id.dueDate), withText(containsString(expectedDateString)), hasSibling(allOf(withId(R.id.discussionTitle), withText(topicTitle)))) onView(matcher).scrollTo().assertDisplayed() } + + fun assertCheckpointDueDates(topicTitle: String, expectedDateString: String) { + val matcher = allOf( + withId(R.id.checkpointDueDates), + withText(containsString(expectedDateString)), + hasSibling(allOf(withId(R.id.discussionTitle), withText(topicTitle))) + ) + onView(matcher).scrollTo().assertDisplayed() + } + + fun assertPointsDisplayed(topicTitle: String, points: Int) { + val matcher = allOf( + withId(R.id.readUnreadCounts), + withText(containsString("$points pts")), + hasSibling(allOf(withId(R.id.discussionTitle), withText(topicTitle))) + ) + onView(matcher).scrollTo().assertDisplayed() + } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileChooserPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileChooserPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileChooserPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileChooserPage.kt index 6ae46f177b..5fce43521d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileChooserPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileChooserPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.doesNotExist diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileListPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileListPage.kt index cec5e6fb4d..ccd1da8500 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/FileListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.appcompat.widget.AppCompatButton import androidx.test.espresso.Espresso diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GoToQuizPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GoToQuizPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/GoToQuizPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GoToQuizPage.kt index 9d6cc3af2c..d60f3d9696 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GoToQuizPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GoToQuizPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import com.instructure.espresso.ModuleItemInteractions import com.instructure.espresso.OnViewWithText diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GradesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GradesPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/GradesPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GradesPage.kt index 1bb0b641b4..3c59c3d673 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GradesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GradesPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.contrib.RecyclerViewActions diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GroupBrowserPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GroupBrowserPage.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/GroupBrowserPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GroupBrowserPage.kt index 3ea8d62fc5..9792cd6bd8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GroupBrowserPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/GroupBrowserPage.kt @@ -1,4 +1,4 @@ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HelpPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/HelpPage.kt similarity index 91% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/HelpPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/HelpPage.kt index cfe0f2c22b..edef884447 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HelpPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/HelpPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.app.Instrumentation import androidx.test.espresso.Espresso @@ -43,17 +43,15 @@ import com.instructure.espresso.typeText import com.instructure.student.R import org.hamcrest.CoreMatchers -// This is a little hokey, as the options that appear are somewhat governed by the results of -// the /api/v1/accounts/self/help_links call. If that changes a lot over time (thus breaking -// this test), we can back off to some easier test like "some options are visible". class HelpPage : BasePage(R.id.helpDialog) { + private val askInstructorLabel by OnViewWithText(R.string.askInstructor) private val searchGuidesLabel by OnViewWithText(R.string.searchGuides) - private val reportProblemLabel by OnViewWithStringTextIgnoreCase("Report a problem") + private val reportProblemLabel by OnViewWithStringTextIgnoreCase("Report a Problem") private val submitFeatureLabel by OnViewWithStringTextIgnoreCase("Submit a Feature Idea") private val shareLoveLabel by OnViewWithText(R.string.shareYourLove) - fun verifyAskAQuestion(course: Course, question: String) { + fun assertAskYourInstructorDialogDetails(course: Course, question: String) { askInstructorLabel.scrollTo().click() waitForView(withText(course.name)).assertDisplayed() // Verify that our course is selected in the spinner onView(withId(R.id.message)).scrollTo().perform(withCustomConstraints(typeText(question), isDisplayingAtLeast(1))) @@ -62,7 +60,7 @@ class HelpPage : BasePage(R.id.helpDialog) { onView(containsTextCaseInsensitive("Send")).assertDisplayed() } - fun verifyAskAQuestion(course: CourseApiModel, question: String) { + private fun assertAskYourInstructorDialogDetails(course: CourseApiModel, question: String) { askInstructorLabel.scrollTo().click() waitForView(withText(course.name)).assertDisplayed() // Verify that our course is selected in the spinner onView(withId(R.id.message)).scrollTo().perform(withCustomConstraints(typeText(question), isDisplayingAtLeast(1))) @@ -72,7 +70,7 @@ class HelpPage : BasePage(R.id.helpDialog) { } fun sendQuestionToInstructor(course: CourseApiModel, question: String) { - verifyAskAQuestion(course, question) + assertAskYourInstructorDialogDetails(course, question) onView(containsTextCaseInsensitive("Send")).click() } @@ -80,14 +78,14 @@ class HelpPage : BasePage(R.id.helpDialog) { searchGuidesLabel.scrollTo().click() } - fun verifyReportAProblem(subject: String, description: String) { + fun assertReportProblemDialogDetails(subject: String, description: String) { reportProblemLabel.scrollTo().click() onView(withId(R.id.subjectEditText)).typeText(subject) Espresso.closeSoftKeyboard() onView(withId(R.id.descriptionEditText)).typeText(description) Espresso.closeSoftKeyboard() // Let's just make sure that the "Send" button is displayed, rather than actually pressing it - onView(containsTextCaseInsensitive("Send")).scrollTo().assertDisplayed() + assertSendReportProblemButtonDisplayed() } fun assertReportProblemDialogDisplayed() { @@ -102,6 +100,10 @@ class HelpPage : BasePage(R.id.helpDialog) { onView(containsTextCaseInsensitive("Send")).scrollTo().click() } + private fun assertSendReportProblemButtonDisplayed() { + onView(containsTextCaseInsensitive("Send")).scrollTo().assertDisplayed() + } + fun clickShareLoveLabel() { shareLoveLabel.scrollTo().click() } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/LeftSideNavigationDrawerPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/LeftSideNavigationDrawerPage.kt index 6b5bed87d4..0f94aa3466 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/LeftSideNavigationDrawerPage.kt @@ -1,4 +1,4 @@ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import androidx.appcompat.widget.SwitchCompat diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ModuleProgressionPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModuleProgressionPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ModuleProgressionPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModuleProgressionPage.kt index c6e6aaca19..5d7d79c96c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ModuleProgressionPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModuleProgressionPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.AmbiguousViewMatcherException import androidx.test.espresso.Espresso.onView diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ModulesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModulesPage.kt similarity index 84% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ModulesPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModulesPage.kt index 5578e2e440..3a2f540655 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ModulesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ModulesPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.PerformException @@ -26,6 +26,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast import androidx.test.espresso.matcher.ViewMatchers.withChild import androidx.test.espresso.matcher.ViewMatchers.withText +import com.instructure.canvas.espresso.ImageViewDrawableMatcher import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.canvas.espresso.withCustomConstraints import com.instructure.canvasapi2.models.Assignment @@ -48,7 +49,6 @@ import com.instructure.espresso.scrollTo import com.instructure.espresso.waitForCheck import com.instructure.pandautils.utils.color import com.instructure.student.R -import com.instructure.student.ui.utils.ImageViewDrawableMatcher import org.hamcrest.Matchers.allOf class ModulesPage : BasePage(R.id.modulesPage) { @@ -111,12 +111,38 @@ class ModulesPage : BasePage(R.id.modulesPage) { } fun assertPossiblePointsNotDisplayed(name: String) { - val matcher = withParent(hasSibling(withChild(withId(R.id.title) + withText(name)))) + withId(R.id.points) + val matcher = allOf( + withId(R.id.points), + hasSibling(withChild(withId(R.id.title) + withText(name))) + ) scrollRecyclerView(R.id.listView, matcher) onView(matcher).assertNotDisplayed() } + fun assertCheckpointDatesDisplayed(discussionTitle: String, expectedDateTexts: List) { + // First scroll to the discussion item + scrollRecyclerView(R.id.listView, withText(discussionTitle)) + + // Find the checkpointDatesContainer within the same parent as the title + val checkpointContainerMatcher = allOf( + withId(R.id.checkpointDatesContainer), + hasSibling(withChild(withId(R.id.title) + withText(discussionTitle))) + ) + + // Assert that the checkpoint dates container is visible (not GONE) + onView(checkpointContainerMatcher).check(matches(isDisplayed())) + + // Assert each expected date text is displayed within the container + expectedDateTexts.forEach { dateText -> + val dateTextMatcher = allOf( + withText(dateText), + withParent(checkpointContainerMatcher) + ) + onView(dateTextMatcher).check(matches(isDisplayed())) + } + } + /** * It is occasionally the case that we need to click a few extra buttons to get "fully" into * the item. Thus the [extraClickIds] vararg param. diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NotificationPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/NotificationPage.kt similarity index 85% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/NotificationPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/NotificationPage.kt index 97747458eb..910c684e27 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NotificationPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/NotificationPage.kt @@ -14,10 +14,11 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.Espresso.onView import androidx.test.espresso.NoMatchingViewException +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.matcher.ViewMatchers.hasSibling import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.canvas.espresso.refresh @@ -44,6 +45,11 @@ class NotificationPage : BasePage() { onView(matcher).assertDisplayed() } + fun assertNotificationNotDisplayed(notificationTitle: String) { + val matcher = allOf(withText(notificationTitle), withAncestor(R.id.listView)) + onView(matcher).check(doesNotExist()) + } + fun assertHasGrade(title: String, grade: String) { val matcher = allOf(containsTextCaseInsensitive(title.dropLast(1)) + hasSibling(withId(R.id.description) + withText("Grade: $grade"))) onView(matcher).scrollTo().assertDisplayed() @@ -59,6 +65,11 @@ class NotificationPage : BasePage() { onView(matcher).scrollTo().assertDisplayed() } + fun assertCheckpointLabel(title: String, checkpointLabel: String) { + val matcher = allOf(containsTextCaseInsensitive(title.dropLast(1)) + hasSibling(withId(R.id.checkpointLabel) + withText(checkpointLabel))) + onView(matcher).scrollTo().assertDisplayed() + } + fun clickNotification(title: String) { val matcher = withText(title) scrollRecyclerView(R.id.listView, matcher) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageDetailsPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageDetailsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageDetailsPage.kt index 2aca1f131e..f7e9d5e01f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageListPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageListPage.kt index 430678fc1f..0424bc4619 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PageListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PairObserverPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PairObserverPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PairObserverPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PairObserverPage.kt index d8a613c064..d692cd4016 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PairObserverPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PairObserverPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PandaAvatarPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PandaAvatarPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PandaAvatarPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PandaAvatarPage.kt index 5bb261d7ef..235df4a65d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PandaAvatarPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PandaAvatarPage.kt @@ -1,4 +1,4 @@ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withText diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PeopleListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PeopleListPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PeopleListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PeopleListPage.kt index 6254ddc991..f8814d9c37 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PeopleListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PeopleListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import androidx.recyclerview.widget.RecyclerView diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PersonDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PersonDetailsPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PersonDetailsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PersonDetailsPage.kt index c955c7fd1a..de48f1026e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PersonDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PersonDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PickerSubmissionUploadPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PickerSubmissionUploadPage.kt similarity index 92% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PickerSubmissionUploadPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PickerSubmissionUploadPage.kt index a37fd598af..3c96301b25 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PickerSubmissionUploadPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PickerSubmissionUploadPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import com.instructure.espresso.OnViewWithId @@ -33,7 +33,6 @@ class PickerSubmissionUploadPage : BasePage(R.id.pickerSubmissionUploadPage) { private val deviceIcon by OnViewWithId(R.id.sourceDeviceIcon) private val cameraIcon by OnViewWithId(R.id.sourceCameraIcon) private val galleryIcon by OnViewWithId(R.id.sourceGalleryIcon) - private val scannerIcon by OnViewWithId(R.id.sourceDocumentScanningIcon) private val deleteButton by OnViewWithId(R.id.deleteButton) fun chooseDevice() { @@ -48,10 +47,6 @@ class PickerSubmissionUploadPage : BasePage(R.id.pickerSubmissionUploadPage) { galleryIcon.click() } - fun chooseScanner() { - scannerIcon.click() - } - fun waitForSubmitButtonToAppear() { waitForViewWithText(R.string.submit) } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ProfileSettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ProfileSettingsPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ProfileSettingsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ProfileSettingsPage.kt index fa6d671d36..750c32bd45 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ProfileSettingsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ProfileSettingsPage.kt @@ -1,4 +1,4 @@ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PushNotificationsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PushNotificationsPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/PushNotificationsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PushNotificationsPage.kt index b4d1888d2e..fa464477dd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PushNotificationsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/PushNotificationsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/QRLoginPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QRLoginPage.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/QRLoginPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QRLoginPage.kt index e59ec44e5f..1fbb8871e4 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/QRLoginPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QRLoginPage.kt @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . * - */ package com.instructure.student.ui.pages + */ package com.instructure.student.ui.pages.classic import com.instructure.espresso.OnViewWithContentDescription import com.instructure.espresso.OnViewWithId diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/QuizListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizListPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/QuizListPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizListPage.kt index 8a88074602..bbe2201c8b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/QuizListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import androidx.test.espresso.action.ViewActions.swipeDown diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/QuizTakingPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizTakingPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/QuizTakingPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizTakingPage.kt index 4e9a92f0da..af523304f5 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/QuizTakingPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/QuizTakingPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.web.webdriver.Locator diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/RemoteConfigSettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/RemoteConfigSettingsPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/RemoteConfigSettingsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/RemoteConfigSettingsPage.kt index 5edda4a5ef..73ea2f4b18 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/RemoteConfigSettingsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/RemoteConfigSettingsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import android.widget.EditText diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionStatusPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ShareExtensionStatusPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionStatusPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ShareExtensionStatusPage.kt index 83d8ba9a25..35c3898cc7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionStatusPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ShareExtensionStatusPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import com.instructure.espresso.WaitForViewWithId import com.instructure.espresso.assertHasText diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionTargetPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ShareExtensionTargetPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionTargetPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ShareExtensionTargetPage.kt index 0d0be79fae..259f18a3bb 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionTargetPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/ShareExtensionTargetPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.Espresso.onData import androidx.test.espresso.assertion.ViewAssertions diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/StudentAssignmentDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt similarity index 65% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/StudentAssignmentDetailsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt index 48d5c0c588..dba4b5084b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/StudentAssignmentDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/StudentAssignmentDetailsPage.kt @@ -13,10 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.appcompat.widget.AppCompatButton +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.hasAnyDescendant +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.test.espresso.Espresso +import androidx.test.espresso.action.ViewActions import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage @@ -36,7 +43,7 @@ import com.instructure.espresso.typeText import com.instructure.student.R import org.hamcrest.Matchers.allOf -class StudentAssignmentDetailsPage(moduleItemInteractions: ModuleItemInteractions): AssignmentDetailsPage(moduleItemInteractions) { +class StudentAssignmentDetailsPage(moduleItemInteractions: ModuleItemInteractions, composeTestRule: ComposeTestRule): AssignmentDetailsPage(moduleItemInteractions, composeTestRule) { fun addBookmark(bookmarkName: String) { openOverflowMenu() @@ -60,6 +67,24 @@ class StudentAssignmentDetailsPage(moduleItemInteractions: ModuleItemInteraction onView(viewMatcher).click() } + fun assertDiscussionCheckpointDetailsOnDetailsPage(checkpointText: String, dueAt: String) + { + try { + composeTestRule.onNode(hasText(dueAt) and hasAnyAncestor(hasTestTag("dueDateColumn-$checkpointText") and hasAnyDescendant(hasTestTag("dueDateHeaderText-$checkpointText")))).assertIsDisplayed() + } catch (e: AssertionError) { + Espresso.onView(withId(R.id.dueComposeView)).perform(ViewActions.scrollTo()) + composeTestRule.waitForIdle() + } + + composeTestRule.onNode(hasTestTag("dueDateHeaderText-$checkpointText"), useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNode(hasText(dueAt) and hasAnyAncestor(hasTestTag("dueDateColumn-$checkpointText") and hasAnyDescendant(hasTestTag("dueDateHeaderText-$checkpointText")))).assertIsDisplayed() + } + + fun assertCheckpointGradesView(checkpointName: String, checkpointGrade: String) { + composeTestRule.onNode(hasTestTag("checkpointName") and hasText(checkpointName), useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNode(hasTestTag("checkpointGrade") and hasText(checkpointGrade), useUnmergedTree = true).assertIsDisplayed() + } + //OfflineMethod fun assertDetailsNotAvailableOffline() { onView(withId(R.id.notAvailableIcon) + withAncestor(R.id.moduleProgressionPage)).assertDisplayed() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsEmptyContentPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsEmptyContentPage.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsEmptyContentPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsEmptyContentPage.kt index 3500562c7c..3bb64d00f3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsEmptyContentPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsEmptyContentPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import com.instructure.espresso.page.BasePage import com.instructure.student.R diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsPage.kt index d969b499f7..48c7f51907 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SubmissionDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SubmissionDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.action.ViewActions.click @@ -52,7 +52,7 @@ import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.espresso.replaceText import com.instructure.student.R -import com.instructure.student.ui.pages.renderPages.SubmissionCommentsRenderPage +import com.instructure.student.ui.rendertests.renderpages.SubmissionCommentsRenderPage import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.anyOf import org.hamcrest.Matchers.containsString diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyllabusPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SyllabusPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyllabusPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SyllabusPage.kt index 00114048ff..7fc11742d9 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyllabusPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/SyllabusPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TextSubmissionUploadPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/TextSubmissionUploadPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/TextSubmissionUploadPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/TextSubmissionUploadPage.kt index bb5e6e1d91..c5095fca40 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TextSubmissionUploadPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/TextSubmissionUploadPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView import com.instructure.canvas.espresso.explicitClick diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/TodoPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/TodoPage.kt index 2ae4b22f62..fde50e7e21 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/TodoPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.matcher.ViewMatchers.withText diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/UrlSubmissionUploadPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/UrlSubmissionUploadPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/UrlSubmissionUploadPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/UrlSubmissionUploadPage.kt index e5eddf16b5..6d5cdb42e3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/UrlSubmissionUploadPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/UrlSubmissionUploadPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic import android.view.View import androidx.test.espresso.UiController diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ElementaryCoursePage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ElementaryCoursePage.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ElementaryCoursePage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ElementaryCoursePage.kt index 5254ab75c0..38de21de61 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ElementaryCoursePage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ElementaryCoursePage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic.k5 import com.instructure.espresso.assertDisplayed import com.instructure.espresso.matchers.WaitForViewMatcher diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ElementaryDashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ElementaryDashboardPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ElementaryDashboardPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ElementaryDashboardPage.kt index e70ea4f77e..94c79b7972 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ElementaryDashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ElementaryDashboardPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic.k5 import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HomeroomPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/HomeroomPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/HomeroomPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/HomeroomPage.kt index 86c3a2a0db..7df4081f6e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HomeroomPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/HomeroomPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic.k5 import androidx.test.espresso.PerformException import androidx.test.espresso.assertion.ViewAssertions diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ImportantDatesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ImportantDatesPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ImportantDatesPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ImportantDatesPage.kt index 8958de26f9..6a7e466a4c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ImportantDatesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ImportantDatesPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic.k5 import android.view.View import androidx.test.espresso.NoMatchingViewException diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ResourcesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ResourcesPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/ResourcesPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ResourcesPage.kt index bd6caa932d..f2e4978081 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ResourcesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/ResourcesPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic.k5 import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/SchedulePage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/SchedulePage.kt index 0d6439244f..c46a47aaed 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/k5/SchedulePage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages +package com.instructure.student.ui.pages.classic.k5 import android.view.View import androidx.test.espresso.NoMatchingViewException diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/ManageOfflineContentPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/ManageOfflineContentPage.kt index 1af690a3dd..a15de9f163 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/ManageOfflineContentPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.ui.pages.offline +package com.instructure.student.ui.pages.classic.offline import androidx.test.espresso.Espresso import androidx.test.espresso.contrib.RecyclerViewActions diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/NativeDiscussionDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/NativeDiscussionDetailsPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/NativeDiscussionDetailsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/NativeDiscussionDetailsPage.kt index 8cc3233183..073580ce53 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/NativeDiscussionDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/NativeDiscussionDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages.offline +package com.instructure.student.ui.pages.classic.offline import android.os.SystemClock.sleep import androidx.test.espresso.Espresso @@ -51,7 +51,7 @@ import com.instructure.espresso.page.withId import com.instructure.espresso.page.withText import com.instructure.espresso.scrollTo import com.instructure.student.R -import com.instructure.student.ui.pages.WebViewTextCheck +import com.instructure.student.ui.pages.classic.WebViewTextCheck import com.instructure.student.ui.utils.TypeInRCETextEditor import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.containsString diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/OfflineSyncSettingsPage.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/OfflineSyncSettingsPage.kt index d27445850e..46060f966d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/OfflineSyncSettingsPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.ui.pages.offline +package com.instructure.student.ui.pages.classic.offline import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isChecked diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/SyncProgressPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/SyncProgressPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/SyncProgressPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/SyncProgressPage.kt index dfb0cd3307..35e251c694 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/SyncProgressPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/classic/offline/SyncProgressPage.kt @@ -15,12 +15,13 @@ * */ -package com.instructure.student.ui.pages.offline +package com.instructure.student.ui.pages.classic.offline import android.widget.TextView import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.hasSibling import com.instructure.canvas.espresso.containsTextCaseInsensitive +import com.instructure.canvas.espresso.getView import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertContainsText import com.instructure.espresso.assertDisplayed @@ -35,7 +36,6 @@ import com.instructure.espresso.page.withId import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.pandautils.R -import com.instructure.student.ui.utils.getView class SyncProgressPage : BasePage(R.id.syncProgressPage) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceDetailsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/ConferenceDetailsRenderTest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceDetailsRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/ConferenceDetailsRenderTest.kt index f6d6478171..652e7889f6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceDetailsRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/ConferenceDetailsRenderTest.kt @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Conference import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsRepositoryFragment import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsViewState import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceRecordingViewState +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceListRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/ConferenceListRenderTest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceListRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/ConferenceListRenderTest.kt index 794fd1fcef..4f46583e3e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceListRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/ConferenceListRenderTest.kt @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import android.graphics.Color import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListItemViewState import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListRepositoryFragment import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListViewState +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/DiscussionSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/DiscussionSubmissionViewRenderTest.kt similarity index 91% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/DiscussionSubmissionViewRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/DiscussionSubmissionViewRenderTest.kt index b76a80cf54..ef4aaa124a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/DiscussionSubmissionViewRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/DiscussionSubmissionViewRenderTest.kt @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.DiscussionSubmissionViewFragment -import com.instructure.student.ui.pages.renderPages.DiscussionSubmissionViewRenderPage +import com.instructure.student.ui.rendertests.renderpages.DiscussionSubmissionViewRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/MediaSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/MediaSubmissionViewRenderTest.kt similarity index 92% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/MediaSubmissionViewRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/MediaSubmissionViewRenderTest.kt index 7444707979..bc97fd33d8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/MediaSubmissionViewRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/MediaSubmissionViewRenderTest.kt @@ -14,19 +14,19 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import android.net.Uri import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.espresso.assertDisplayed import com.instructure.espresso.assertNotDisplayed import com.instructure.espresso.click import com.instructure.pandautils.utils.PandaPrefs -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsContentType import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.MediaSubmissionViewFragment -import com.instructure.student.ui.pages.renderPages.MediaSubmissionViewRenderPage +import com.instructure.student.ui.rendertests.renderpages.MediaSubmissionViewRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import org.junit.runner.RunWith diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PairObserverRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/PairObserverRenderTest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PairObserverRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/PairObserverRenderTest.kt index fb226e6cbb..241a25db98 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PairObserverRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/PairObserverRenderTest.kt @@ -14,12 +14,12 @@ * limitations under the License. * */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.settings.pairobserver.PairObserverModel import com.instructure.student.mobius.settings.pairobserver.ui.PairObserverFragment +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PickerSubmissionUploadRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/PickerSubmissionUploadRenderTest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PickerSubmissionUploadRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/PickerSubmissionUploadRenderTest.kt index b1a7a71639..18127807f0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PickerSubmissionUploadRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/PickerSubmissionUploadRenderTest.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -29,13 +29,13 @@ import com.instructure.espresso.assertHasText import com.instructure.espresso.assertNotDisplayed import com.instructure.espresso.assertVisible import com.instructure.student.R -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionMode import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerListItemViewState import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerSubmissionUploadFragment import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerSubmissionUploadViewState import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerVisibilities -import com.instructure.student.ui.pages.renderPages.PickerSubmissionUploadRenderPage +import com.instructure.student.ui.rendertests.renderpages.PickerSubmissionUploadRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/QuizSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/QuizSubmissionViewRenderTest.kt similarity index 90% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/QuizSubmissionViewRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/QuizSubmissionViewRenderTest.kt index 30deffe789..1dbd51ad11 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/QuizSubmissionViewRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/QuizSubmissionViewRenderTest.kt @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import com.instructure.espresso.assertNotDisplayed import com.instructure.espresso.page.onViewWithId import com.instructure.student.R -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.QuizSubmissionViewFragment -import com.instructure.student.ui.pages.renderPages.QuizSubmissionViewRenderPage +import com.instructure.student.ui.rendertests.renderpages.QuizSubmissionViewRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionCommentsRenderTest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionCommentsRenderTest.kt index 10fbf51a03..87288d27ab 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionCommentsRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.espresso.Espresso import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -26,14 +26,14 @@ import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.models.User -import com.instructure.student.espresso.StudentRenderTest +import com.instructure.pandautils.room.studentdb.StudentDb +import com.instructure.pandautils.room.studentdb.entities.CreatePendingSubmissionCommentEntity import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.CommentItemState import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.SubmissionCommentsViewState import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.ui.SubmissionCommentsFragment import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsTabData -import com.instructure.pandautils.room.studentdb.StudentDb -import com.instructure.pandautils.room.studentdb.entities.CreatePendingSubmissionCommentEntity -import com.instructure.student.ui.pages.renderPages.SubmissionCommentsRenderPage +import com.instructure.student.ui.rendertests.renderpages.SubmissionCommentsRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import junit.framework.Assert.assertTrue diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsEmptyContentRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionDetailsEmptyContentRenderTest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsEmptyContentRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionDetailsEmptyContentRenderTest.kt index 40d39e788b..5f4a82a5b5 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsEmptyContentRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionDetailsEmptyContentRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.Assignment @@ -21,9 +21,9 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.LockInfo import com.instructure.canvasapi2.models.LockedModule import com.instructure.student.R -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.SubmissionDetailsEmptyContentModel import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.ui.SubmissionDetailsEmptyContentFragment +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionDetailsRenderTest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionDetailsRenderTest.kt index ad15b6f54a..e87073ff76 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionDetailsRenderTest.kt @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import android.content.pm.ActivityInfo import androidx.test.espresso.action.GeneralLocation import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Submission @@ -27,9 +27,9 @@ import com.instructure.canvasapi2.utils.DateHelper import com.instructure.espresso.assertGone import com.instructure.espresso.assertVisible import com.instructure.espresso.click -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsModel import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsRepositoryFragment +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionFilesRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionFilesRenderTest.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionFilesRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionFilesRenderTest.kt index ab45b21fbb..70ff8ed801 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionFilesRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionFilesRenderTest.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import android.graphics.Color import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -23,12 +23,12 @@ import com.instructure.espresso.assertGone import com.instructure.espresso.assertHasText import com.instructure.espresso.assertVisible import com.instructure.student.R -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.files.SubmissionFileData import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.files.SubmissionFilesViewState import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.files.ui.SubmissionFilesFragment import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsTabData -import com.instructure.student.ui.pages.renderPages.SubmissionFilesRenderPage +import com.instructure.student.ui.rendertests.renderpages.SubmissionFilesRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionRubricRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionRubricRenderTest.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionRubricRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionRubricRenderTest.kt index 654d0437b3..e9c3e63113 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionRubricRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SubmissionRubricRenderTest.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import android.graphics.Color import androidx.test.espresso.assertion.ViewAssertions.matches @@ -22,6 +22,7 @@ import androidx.test.espresso.matcher.RootMatchers import androidx.test.espresso.matcher.ViewMatchers.hasChildCount import androidx.test.espresso.matcher.ViewMatchers.isSelected import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvas.espresso.assertFontSizeSP import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.RubricCriterion import com.instructure.canvasapi2.models.RubricCriterionRating @@ -34,15 +35,14 @@ import com.instructure.espresso.assertVisible import com.instructure.espresso.click import com.instructure.espresso.page.onViewWithText import com.instructure.pandautils.features.assignments.details.mobius.gradeCell.GradeCellViewState -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.RatingData import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.RubricListData import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.SubmissionRubricModel import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.SubmissionRubricViewState import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.ui.SubmissionRubricFragment import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsTabData -import com.instructure.student.ui.pages.renderPages.SubmissionRubricRenderPage -import com.instructure.student.ui.utils.assertFontSizeSP +import com.instructure.student.ui.rendertests.renderpages.SubmissionRubricRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.CoreMatchers.not diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SyllabusRenderTest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SyllabusRenderTest.kt index 8c56f5169f..3a43da48eb 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/SyllabusRenderTest.kt @@ -14,16 +14,16 @@ * limitations under the License. * */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.syllabus.SyllabusModel import com.instructure.student.mobius.syllabus.ui.SyllabusRepositoryFragment +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionUploadRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/TextSubmissionUploadRenderTest.kt similarity index 92% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionUploadRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/TextSubmissionUploadRenderTest.kt index afe3a91efa..254985fa9d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionUploadRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/TextSubmissionUploadRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers @@ -21,9 +21,9 @@ import androidx.test.espresso.matcher.ViewMatchers.isEnabled import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Course import com.instructure.espresso.waitForCheck -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submission.text.ui.TextSubmissionUploadFragment -import com.instructure.student.ui.pages.renderPages.TextSubmissionUploadRenderPage +import com.instructure.student.ui.rendertests.renderpages.TextSubmissionUploadRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.CoreMatchers.not import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/TextSubmissionViewRenderTest.kt similarity index 89% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionViewRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/TextSubmissionViewRenderTest.kt index 2ebbc05737..143aa728de 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionViewRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/TextSubmissionViewRenderTest.kt @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests -import com.instructure.canvas.espresso.Stub -import com.instructure.student.espresso.StudentRenderTest +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.TextSubmissionViewFragment -import com.instructure.student.ui.pages.renderPages.TextSubmissionViewRenderPage +import com.instructure.student.ui.rendertests.renderpages.TextSubmissionViewRenderPage +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UploadStatusSubmissionRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UploadStatusSubmissionRenderTest.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UploadStatusSubmissionRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UploadStatusSubmissionRenderTest.kt index 24522803ee..1f12a43d2b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UploadStatusSubmissionRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UploadStatusSubmissionRenderTest.kt @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.student.espresso.StudentRenderTest +import com.instructure.pandautils.room.studentdb.entities.CreateFileSubmissionEntity import com.instructure.student.mobius.assignmentDetails.submission.file.UploadStatusSubmissionModel import com.instructure.student.mobius.assignmentDetails.submission.file.ui.UploadStatusSubmissionFragment -import com.instructure.pandautils.room.studentdb.entities.CreateFileSubmissionEntity +import com.instructure.student.ui.utils.StudentRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionUploadRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UrlSubmissionUploadRenderTest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionUploadRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UrlSubmissionUploadRenderTest.kt index 4b1890beff..d10cccecb9 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionUploadRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UrlSubmissionUploadRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isEnabled @@ -23,8 +23,8 @@ import com.instructure.canvasapi2.models.Course import com.instructure.espresso.assertHasText import com.instructure.espresso.replaceText import com.instructure.espresso.waitForCheck -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submission.url.ui.UrlSubmissionUploadFragment +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers.not import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UrlSubmissionViewRenderTest.kt similarity index 93% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionViewRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UrlSubmissionViewRenderTest.kt index bdf618797a..d032231807 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionViewRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/UrlSubmissionViewRenderTest.kt @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests +package com.instructure.student.ui.rendertests import com.instructure.espresso.assertCompletelyDisplayed import com.instructure.espresso.assertHasText -import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.UrlSubmissionViewFragment +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/ConferenceDetailsRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceDetailsRenderPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/ConferenceDetailsRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceDetailsRenderPage.kt index a93d63960a..4a58351281 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/ConferenceDetailsRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceDetailsRenderPage.kt @@ -14,10 +14,11 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.withAlpha +import com.instructure.canvas.espresso.assertIsRefreshing import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed import com.instructure.espresso.assertGone @@ -30,8 +31,7 @@ import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.student.R import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceRecordingViewState -import com.instructure.student.ui.pages.ConferenceDetailsPage -import com.instructure.student.ui.utils.assertIsRefreshing +import com.instructure.student.ui.pages.classic.ConferenceDetailsPage import org.hamcrest.Matchers.allOf class ConferenceDetailsRenderPage : ConferenceDetailsPage() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/ConferenceListRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceListRenderPage.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/ConferenceListRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceListRenderPage.kt index 89c9667c14..afe553df23 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/ConferenceListRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/ConferenceListRenderPage.kt @@ -14,9 +14,10 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import com.instructure.canvas.espresso.assertIsRefreshing import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed @@ -31,8 +32,7 @@ import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.student.R import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListItemViewState -import com.instructure.student.ui.pages.ConferenceListPage -import com.instructure.student.ui.utils.assertIsRefreshing +import com.instructure.student.ui.pages.classic.ConferenceListPage import org.hamcrest.Matchers.allOf class ConferenceListRenderPage : ConferenceListPage() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/DiscussionSubmissionViewRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/DiscussionSubmissionViewRenderPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/DiscussionSubmissionViewRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/DiscussionSubmissionViewRenderPage.kt index 4b871dac3e..c3d73f6b13 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/DiscussionSubmissionViewRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/DiscussionSubmissionViewRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/MediaSubmissionViewRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/MediaSubmissionViewRenderPage.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/MediaSubmissionViewRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/MediaSubmissionViewRenderPage.kt index e5b4063b04..ea76599b8d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/MediaSubmissionViewRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/MediaSubmissionViewRenderPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.WaitForViewWithId diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/PairObserverRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PairObserverRenderPage.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/PairObserverRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PairObserverRenderPage.kt index a008bce279..c08fb787ce 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/PairObserverRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PairObserverRenderPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.canvas.espresso.waitForMatcherWithSleeps import com.instructure.espresso.OnViewWithId @@ -25,7 +25,7 @@ import com.instructure.espresso.page.withId import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.student.R -import com.instructure.student.ui.pages.PairObserverPage +import com.instructure.student.ui.pages.classic.PairObserverPage import org.hamcrest.Matchers.allOf class PairObserverRenderPage : PairObserverPage() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/PickerSubmissionUploadRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PickerSubmissionUploadRenderPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/PickerSubmissionUploadRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PickerSubmissionUploadRenderPage.kt index c75c41a01a..7a3d2cf467 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/PickerSubmissionUploadRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/PickerSubmissionUploadRenderPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.hasDescendant diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/QuizSubmissionViewRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/QuizSubmissionViewRenderPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/QuizSubmissionViewRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/QuizSubmissionViewRenderPage.kt index d1f7f0422c..a324aa4825 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/QuizSubmissionViewRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/QuizSubmissionViewRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionCommentsRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionCommentsRenderPage.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionCommentsRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionCommentsRenderPage.kt index 394559aabc..c6ece490cc 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionCommentsRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionCommentsRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import android.os.SystemClock.sleep import android.view.View diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsEmptyContentRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionDetailsEmptyContentRenderPage.kt similarity index 92% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsEmptyContentRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionDetailsEmptyContentRenderPage.kt index f0814650f1..d1ddee1f6b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsEmptyContentRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionDetailsEmptyContentRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.annotation.StringRes import androidx.test.espresso.assertion.ViewAssertions.matches @@ -23,7 +23,7 @@ import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertContainsText import com.instructure.espresso.assertHasText import com.instructure.student.R -import com.instructure.student.ui.pages.SubmissionDetailsEmptyContentPage +import com.instructure.student.ui.pages.classic.SubmissionDetailsEmptyContentPage import org.hamcrest.CoreMatchers.not class SubmissionDetailsEmptyContentRenderPage : SubmissionDetailsEmptyContentPage() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionDetailsRenderPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionDetailsRenderPage.kt index 0328828a1f..b07b53c8f7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionDetailsRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionDetailsRenderPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import android.view.View import androidx.test.espresso.Espresso.onData @@ -42,7 +42,7 @@ import com.instructure.espresso.page.withText import com.instructure.espresso.scrollTo import com.instructure.espresso.waitForCheck import com.instructure.student.R -import com.instructure.student.ui.pages.SubmissionDetailsPage +import com.instructure.student.ui.pages.classic.SubmissionDetailsPage import com.sothree.slidinguppanel.SlidingUpPanelLayout import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.anything diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionFilesRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionFilesRenderPage.kt similarity index 95% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionFilesRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionFilesRenderPage.kt index 8be16aa0a4..338cbce7a0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionFilesRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionFilesRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.page.BasePage diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionRubricRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionRubricRenderPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionRubricRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionRubricRenderPage.kt index 27f746ba57..d69c77ddcd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SubmissionRubricRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SubmissionRubricRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.page.BasePage diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SyllabusRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SyllabusRenderPage.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SyllabusRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SyllabusRenderPage.kt index 8856191098..f35ebeb791 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/SyllabusRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/SyllabusRenderPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.action.ViewActions.swipeLeft import androidx.test.espresso.action.ViewActions.swipeRight @@ -34,7 +34,7 @@ import com.instructure.espresso.page.withId import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.student.R -import com.instructure.student.ui.pages.SyllabusPage +import com.instructure.student.ui.pages.classic.SyllabusPage import org.hamcrest.CoreMatchers import org.hamcrest.Matchers import org.hamcrest.Matchers.allOf diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/TextSubmissionUploadRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/TextSubmissionUploadRenderPage.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/TextSubmissionUploadRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/TextSubmissionUploadRenderPage.kt index 715613fc4f..069e47cafb 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/TextSubmissionUploadRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/TextSubmissionUploadRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.page.BasePage diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/TextSubmissionViewRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/TextSubmissionViewRenderPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/TextSubmissionViewRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/TextSubmissionViewRenderPage.kt index 0d5dc42173..72465dd6f8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/TextSubmissionViewRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/TextSubmissionViewRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UploadStatusSubmissionViewRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UploadStatusSubmissionViewRenderPage.kt similarity index 97% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UploadStatusSubmissionViewRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UploadStatusSubmissionViewRenderPage.kt index 81f50e6a04..b4ca54ecc2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UploadStatusSubmissionViewRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UploadStatusSubmissionViewRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertVisible diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UrlSubmissionUploadRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UrlSubmissionUploadRenderPage.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UrlSubmissionUploadRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UrlSubmissionUploadRenderPage.kt index 2aea428816..0fa583025c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UrlSubmissionUploadRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UrlSubmissionUploadRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.page.BasePage diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UrlSubmissionViewRenderPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UrlSubmissionViewRenderPage.kt similarity index 94% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UrlSubmissionViewRenderPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UrlSubmissionViewRenderPage.kt index a550e30f6d..06b12150ef 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/renderPages/UrlSubmissionViewRenderPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/UrlSubmissionViewRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.pages.renderPages +package com.instructure.student.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.OnViewWithText diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/views/GradeCellRenderTest.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/views/GradeCellRenderTest.kt index 17151a2d39..64e5ddc10b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/rendertests/renderpages/views/GradeCellRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.ui.renderTests.views +package com.instructure.student.ui.rendertests.renderpages.views import android.view.ViewGroup import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -26,7 +26,7 @@ import com.instructure.espresso.page.BasePage import com.instructure.pandautils.features.assignments.details.mobius.gradeCell.GradeCellView import com.instructure.pandautils.features.assignments.details.mobius.gradeCell.GradeCellViewState import com.instructure.student.R -import com.instructure.student.espresso.StudentRenderTest +import com.instructure.student.ui.utils.StudentRenderTest import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import org.junit.runner.RunWith diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/Matchers.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/Matchers.kt deleted file mode 100644 index d388442cfc..0000000000 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/Matchers.kt +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2019 - present Instructure, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.instructure.student.ui.utils - -import android.content.Intent -import android.content.res.Resources -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.drawable.Drawable -import android.view.View -import android.widget.ImageView -import android.widget.TextView -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import androidx.test.espresso.UiController -import androidx.test.espresso.ViewAction -import androidx.test.espresso.ViewInteraction -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.BoundedMatcher -import androidx.test.espresso.matcher.ViewMatchers.assertThat -import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom -import com.instructure.pandautils.utils.ColorUtils -import org.hamcrest.CoreMatchers.`is` -import org.hamcrest.Description -import org.hamcrest.Matcher -import org.hamcrest.TypeSafeMatcher - -fun ViewInteraction.assertLineCount(lineCount: Int) { - val matcher = object : TypeSafeMatcher() { - override fun matchesSafely(item: View): Boolean { - return (item as TextView).lineCount == lineCount - } - - override fun describeTo(description: Description) { - description.appendText("isTextInLines") - } - } - check(matches(matcher)) -} - - -fun ViewInteraction.getView(): View { - lateinit var matchingView: View - perform(object : ViewAction { - override fun getDescription() = "Get View reference" - - override fun getConstraints(): Matcher { - return isAssignableFrom(View::class.java) - } - - override fun perform(uiController: UiController?, view: View) { - matchingView = view - } - }) - return matchingView -} - -fun ViewInteraction.assertCompletelyAbove(other: ViewInteraction) { - val view1 = getView() - val view2 = other.getView() - val location1 = view1.locationOnScreen - val location2 = view2.locationOnScreen - val isAbove = location1[1] + view1.height <= location2[1] - assertThat("completely above", isAbove, `is`(true)) -} - -fun ViewInteraction.assertCompletelyBelow(other: ViewInteraction) { - val view1 = getView() - val view2 = other.getView() - val location1 = view1.locationOnScreen - val location2 = view2.locationOnScreen - val isAbove = location2[1] + view2.height <= location1[1] - assertThat("completely below", isAbove, `is`(true)) -} - -val View.locationOnScreen get() = IntArray(2).apply { getLocationOnScreen(this) } - - -/** - * Asserts that the TextView uses the specified font size in scaled pixels - */ -fun ViewInteraction.assertFontSizeSP(expectedSP: Float) { - val matcher = object : TypeSafeMatcher(View::class.java) { - - override fun matchesSafely(target: View): Boolean { - if (target !is TextView) return false - val actualSP = target.textSize / target.getResources().displayMetrics.scaledDensity - return actualSP.compareTo(expectedSP) == 0 - } - - override fun describeTo(description: Description) { - description.appendText("with fontSize: ${expectedSP}px") - } - } - check(matches(matcher)) -} - -fun ViewInteraction.assertIsRefreshing(isRefreshing: Boolean) { - val matcher = object : BoundedMatcher(SwipeRefreshLayout::class.java) { - - override fun describeTo(description: Description) { - description.appendText(if (isRefreshing) "is refreshing" else "is not refreshing") - } - - override fun matchesSafely(view: SwipeRefreshLayout): Boolean { - return view.isRefreshing == isRefreshing - } - } - check(matches(matcher)) -} - -class IntentActionMatcher(private val intentType: String, private val dataMatcher: String) : TypeSafeMatcher() { - - override fun describeTo(description: Description?) { - description?.appendText("Intent Matcher") - } - - override fun matchesSafely(item: Intent?): Boolean { - return (intentType == item?.action) && (item?.dataString?.contains(dataMatcher) ?: false) - } -} - -// Adapted from https://medium.com/@dbottillo/android-ui-test-espresso-matcher-for-imageview-1a28c832626f -/** - * Matches ImageView (or ImageButton) with the drawable associated with [resourceId]. If [resourceId] < 0, will - * match against "no drawable" / "drawable is null". - * - * If the [color] param is non-null, then the drawable associated with [resourceId] will be colored - * prior to matching. - */ -class ImageViewDrawableMatcher(val resourceId: Int, val color: Int? = null) : TypeSafeMatcher( - ImageView::class.java) { - override fun describeTo(description: Description) { - description.appendText("with drawable from resource id: ") - description.appendValue(resourceId) - } - - override fun matchesSafely(target: View?): Boolean { - if (target !is ImageView) { - return false - } - val imageView = target - if (resourceId < 0) { - return imageView.drawable == null - } - val resources: Resources = target.getContext().getResources() - val expectedDrawable: Drawable = resources.getDrawable(resourceId) ?: return false - if(color != null) { - ColorUtils.colorIt(color, expectedDrawable) - } - val bitmap: Bitmap = getBitmap(imageView.getDrawable()) - val otherBitmap: Bitmap = getBitmap(expectedDrawable) - return bitmap.sameAs(otherBitmap) - } - - private fun getBitmap(drawable: Drawable): Bitmap { - val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, - drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()) - drawable.draw(canvas) - return bitmap - } -} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentActivityTestRule.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentActivityTestRule.kt index f14c13ddba..7106bad06b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentActivityTestRule.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentActivityTestRule.kt @@ -18,13 +18,12 @@ package com.instructure.student.ui.utils import android.app.Activity import android.content.Context -import com.instructure.student.util.CacheControlFlags -import com.instructure.student.util.StudentPrefs import com.instructure.espresso.InstructureActivityTestRule import com.instructure.loginapi.login.util.LoginPrefs import com.instructure.loginapi.login.util.PreviousUsersUtils import com.instructure.pandautils.utils.PandaAppResetter -import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.student.util.CacheControlFlags +import com.instructure.student.util.StudentPrefs class StudentActivityTestRule(activityClass: Class) : InstructureActivityTestRule(activityClass) { @@ -34,9 +33,6 @@ class StudentActivityTestRule(activityClass: Class) : Instructu CacheControlFlags.clearPrefs() PreviousUsersUtils.clear(context) LoginPrefs.clearPrefs() - - // We need to set this true so the theme selector won't stop our tests. - ThemePrefs.themeSelectionShown = true } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt index 4350c4c16a..5ccf520462 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt @@ -19,7 +19,7 @@ package com.instructure.student.ui.utils import androidx.compose.ui.test.junit4.createAndroidComposeRule -import com.instructure.canvas.espresso.common.pages.ReminderPage +import com.instructure.canvas.espresso.common.pages.AssignmentReminderPage import com.instructure.canvas.espresso.common.pages.compose.AssignmentListPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventCreateEditPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventDetailsPage @@ -35,7 +35,10 @@ import com.instructure.canvas.espresso.common.pages.compose.SelectContextPage import com.instructure.canvas.espresso.common.pages.compose.SettingsPage import com.instructure.canvas.espresso.common.pages.compose.SmartSearchPage import com.instructure.canvas.espresso.common.pages.compose.SmartSearchPreferencesPage +import com.instructure.espresso.ModuleItemInteractions +import com.instructure.student.R import com.instructure.student.activity.LoginActivity +import com.instructure.student.ui.pages.classic.StudentAssignmentDetailsPage import org.junit.Rule abstract class StudentComposeTest : StudentTest() { @@ -51,7 +54,7 @@ abstract class StudentComposeTest : StudentTest() { val calendarToDoDetailsPage = CalendarToDoDetailsPage(composeTestRule) val calendarFilterPage = CalendarFilterPage(composeTestRule) val settingsPage = SettingsPage(composeTestRule) - val reminderPage = ReminderPage(composeTestRule) + val assignmentReminderPage = AssignmentReminderPage(composeTestRule) val inboxDetailsPage = InboxDetailsPage(composeTestRule) val inboxComposePage = InboxComposePage(composeTestRule) val recipientPickerPage = RecipientPickerPage(composeTestRule) @@ -60,4 +63,11 @@ abstract class StudentComposeTest : StudentTest() { val smartSearchPreferencesPage = SmartSearchPreferencesPage(composeTestRule) val assignmentListPage = AssignmentListPage(composeTestRule) val inboxSignatureSettingsPage = InboxSignatureSettingsPage(composeTestRule) + val assignmentDetailsPage = StudentAssignmentDetailsPage( + ModuleItemInteractions( + R.id.moduleName, + R.id.next_item, + R.id.prev_item + ), composeTestRule + ) } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/espresso/StudentRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentRenderTest.kt similarity index 67% rename from apps/student/src/androidTest/java/com/instructure/student/espresso/StudentRenderTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentRenderTest.kt index ca812241d3..10fddb2657 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/espresso/StudentRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentRenderTest.kt @@ -14,13 +14,20 @@ * limitations under the License. * */ -package com.instructure.student.espresso +package com.instructure.student.ui.utils import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.student.SingleFragmentTestActivity -import com.instructure.student.ui.pages.renderPages.* -import com.instructure.student.ui.utils.StudentActivityTestRule -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.rendertests.renderpages.ConferenceDetailsRenderPage +import com.instructure.student.ui.rendertests.renderpages.ConferenceListRenderPage +import com.instructure.student.ui.rendertests.renderpages.PairObserverRenderPage +import com.instructure.student.ui.rendertests.renderpages.SubmissionDetailsEmptyContentRenderPage +import com.instructure.student.ui.rendertests.renderpages.SubmissionDetailsRenderPage +import com.instructure.student.ui.rendertests.renderpages.SyllabusRenderPage +import com.instructure.student.ui.rendertests.renderpages.TextSubmissionUploadRenderPage +import com.instructure.student.ui.rendertests.renderpages.UploadStatusSubmissionViewRenderPage +import com.instructure.student.ui.rendertests.renderpages.UrlSubmissionUploadRenderPage +import com.instructure.student.ui.rendertests.renderpages.UrlSubmissionViewRenderPage import org.junit.runner.RunWith // Test from which all Student PageRender/SingleFragmentTestActivity tests will derive diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt index e10448467b..6bcea91db0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt @@ -40,6 +40,7 @@ import com.instructure.canvas.espresso.common.pages.LegalPage import com.instructure.canvas.espresso.common.pages.LoginFindSchoolPage import com.instructure.canvas.espresso.common.pages.LoginLandingPage import com.instructure.canvas.espresso.common.pages.LoginSignInPage +import com.instructure.canvas.espresso.common.pages.WrongDomainPage import com.instructure.espresso.InstructureActivityTestRule import com.instructure.espresso.ModuleItemInteractions import com.instructure.espresso.Searchable @@ -48,63 +49,61 @@ import com.instructure.pandautils.utils.Const import com.instructure.student.BuildConfig import com.instructure.student.R import com.instructure.student.activity.LoginActivity -import com.instructure.student.espresso.TestAppManager -import com.instructure.student.ui.pages.AllCoursesPage -import com.instructure.student.ui.pages.AnnotationCommentListPage -import com.instructure.student.ui.pages.AnnouncementListPage -import com.instructure.student.ui.pages.BookmarkPage -import com.instructure.student.ui.pages.CanvasWebViewPage -import com.instructure.student.ui.pages.ConferenceDetailsPage -import com.instructure.student.ui.pages.ConferenceListPage -import com.instructure.student.ui.pages.CourseBrowserPage -import com.instructure.student.ui.pages.CourseGradesPage -import com.instructure.student.ui.pages.DashboardPage -import com.instructure.student.ui.pages.DiscussionListPage -import com.instructure.student.ui.pages.ElementaryCoursePage -import com.instructure.student.ui.pages.ElementaryDashboardPage -import com.instructure.student.ui.pages.FileChooserPage -import com.instructure.student.ui.pages.FileListPage -import com.instructure.student.ui.pages.GoToQuizPage -import com.instructure.student.ui.pages.GradesPage -import com.instructure.student.ui.pages.GroupBrowserPage -import com.instructure.student.ui.pages.HelpPage -import com.instructure.student.ui.pages.HomeroomPage -import com.instructure.student.ui.pages.ImportantDatesPage -import com.instructure.student.ui.pages.LeftSideNavigationDrawerPage -import com.instructure.student.ui.pages.ModuleProgressionPage -import com.instructure.student.ui.pages.ModulesPage -import com.instructure.student.ui.pages.NotificationPage -import com.instructure.student.ui.pages.PageDetailsPage -import com.instructure.student.ui.pages.PageListPage -import com.instructure.student.ui.pages.PairObserverPage -import com.instructure.student.ui.pages.PandaAvatarPage -import com.instructure.student.ui.pages.PeopleListPage -import com.instructure.student.ui.pages.PersonDetailsPage -import com.instructure.student.ui.pages.PickerSubmissionUploadPage -import com.instructure.student.ui.pages.ProfileSettingsPage -import com.instructure.student.ui.pages.PushNotificationsPage -import com.instructure.student.ui.pages.QRLoginPage -import com.instructure.student.ui.pages.QuizListPage -import com.instructure.student.ui.pages.QuizTakingPage -import com.instructure.student.ui.pages.RemoteConfigSettingsPage -import com.instructure.student.ui.pages.ResourcesPage -import com.instructure.student.ui.pages.SchedulePage -import com.instructure.student.ui.pages.ShareExtensionStatusPage -import com.instructure.student.ui.pages.ShareExtensionTargetPage -import com.instructure.student.ui.pages.StudentAssignmentDetailsPage -import com.instructure.student.ui.pages.SubmissionDetailsPage -import com.instructure.student.ui.pages.SyllabusPage -import com.instructure.student.ui.pages.TextSubmissionUploadPage -import com.instructure.student.ui.pages.TodoPage -import com.instructure.student.ui.pages.UrlSubmissionUploadPage -import com.instructure.student.ui.pages.offline.ManageOfflineContentPage -import com.instructure.student.ui.pages.offline.NativeDiscussionDetailsPage -import com.instructure.student.ui.pages.offline.OfflineSyncSettingsPage -import com.instructure.student.ui.pages.offline.SyncProgressPage +import com.instructure.student.ui.pages.classic.AllCoursesPage +import com.instructure.student.ui.pages.classic.AnnotationCommentListPage +import com.instructure.student.ui.pages.classic.AnnouncementListPage +import com.instructure.student.ui.pages.classic.BookmarkPage +import com.instructure.student.ui.pages.classic.CanvasWebViewPage +import com.instructure.student.ui.pages.classic.ConferenceDetailsPage +import com.instructure.student.ui.pages.classic.ConferenceListPage +import com.instructure.student.ui.pages.classic.CourseBrowserPage +import com.instructure.student.ui.pages.classic.CourseGradesPage +import com.instructure.student.ui.pages.classic.DashboardPage +import com.instructure.student.ui.pages.classic.DiscussionDetailsPage +import com.instructure.student.ui.pages.classic.DiscussionListPage +import com.instructure.student.ui.pages.classic.FileChooserPage +import com.instructure.student.ui.pages.classic.FileListPage +import com.instructure.student.ui.pages.classic.GoToQuizPage +import com.instructure.student.ui.pages.classic.GradesPage +import com.instructure.student.ui.pages.classic.GroupBrowserPage +import com.instructure.student.ui.pages.classic.HelpPage +import com.instructure.student.ui.pages.classic.LeftSideNavigationDrawerPage +import com.instructure.student.ui.pages.classic.ModuleProgressionPage +import com.instructure.student.ui.pages.classic.ModulesPage +import com.instructure.student.ui.pages.classic.NotificationPage +import com.instructure.student.ui.pages.classic.PageDetailsPage +import com.instructure.student.ui.pages.classic.PageListPage +import com.instructure.student.ui.pages.classic.PairObserverPage +import com.instructure.student.ui.pages.classic.PandaAvatarPage +import com.instructure.student.ui.pages.classic.PeopleListPage +import com.instructure.student.ui.pages.classic.PersonDetailsPage +import com.instructure.student.ui.pages.classic.PickerSubmissionUploadPage +import com.instructure.student.ui.pages.classic.ProfileSettingsPage +import com.instructure.student.ui.pages.classic.PushNotificationsPage +import com.instructure.student.ui.pages.classic.QRLoginPage +import com.instructure.student.ui.pages.classic.QuizListPage +import com.instructure.student.ui.pages.classic.QuizTakingPage +import com.instructure.student.ui.pages.classic.RemoteConfigSettingsPage +import com.instructure.student.ui.pages.classic.ShareExtensionStatusPage +import com.instructure.student.ui.pages.classic.ShareExtensionTargetPage +import com.instructure.student.ui.pages.classic.SubmissionDetailsPage +import com.instructure.student.ui.pages.classic.SyllabusPage +import com.instructure.student.ui.pages.classic.TextSubmissionUploadPage +import com.instructure.student.ui.pages.classic.TodoPage +import com.instructure.student.ui.pages.classic.UrlSubmissionUploadPage +import com.instructure.student.ui.pages.classic.k5.ElementaryCoursePage +import com.instructure.student.ui.pages.classic.k5.ElementaryDashboardPage +import com.instructure.student.ui.pages.classic.k5.HomeroomPage +import com.instructure.student.ui.pages.classic.k5.ImportantDatesPage +import com.instructure.student.ui.pages.classic.k5.ResourcesPage +import com.instructure.student.ui.pages.classic.k5.SchedulePage +import com.instructure.student.ui.pages.classic.offline.ManageOfflineContentPage +import com.instructure.student.ui.pages.classic.offline.NativeDiscussionDetailsPage +import com.instructure.student.ui.pages.classic.offline.OfflineSyncSettingsPage +import com.instructure.student.ui.pages.classic.offline.SyncProgressPage import instructure.rceditor.RCETextEditor import org.hamcrest.Matcher import org.hamcrest.core.AllOf -import org.junit.Before import java.io.File abstract class StudentTest : CanvasTest() { @@ -121,7 +120,6 @@ abstract class StudentTest : CanvasTest() { */ val annotationCommentListPage = AnnotationCommentListPage() val announcementListPage = AnnouncementListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn)) - val assignmentDetailsPage = StudentAssignmentDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item)) val bookmarkPage = BookmarkPage() val canvasWebViewPage = CanvasWebViewPage() val courseBrowserPage = CourseBrowserPage() @@ -132,7 +130,7 @@ abstract class StudentTest : CanvasTest() { val courseGradesPage = CourseGradesPage() val dashboardPage = DashboardPage() val leftSideNavigationDrawerPage = LeftSideNavigationDrawerPage() - val discussionDetailsPage = com.instructure.student.ui.pages.DiscussionDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item)) + val discussionDetailsPage = DiscussionDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item)) val nativeDiscussionDetailsPage = NativeDiscussionDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item)) val discussionListPage = DiscussionListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn)) val allCoursesPage = AllCoursesPage() @@ -146,6 +144,7 @@ abstract class StudentTest : CanvasTest() { val loginLandingPage = LoginLandingPage() val canvasNetworkSignInPage = CanvasNetworkSignInPage() val loginSignInPage = LoginSignInPage() + val wrongDomainPage = WrongDomainPage() val moduleProgressionPage = ModuleProgressionPage() val modulesPage = ModulesPage() val notificationPage = NotificationPage() @@ -181,12 +180,6 @@ abstract class StudentTest : CanvasTest() { val manageOfflineContentPage = ManageOfflineContentPage() val syncProgressPage = SyncProgressPage() - @Before - fun setupWorkerFactory() { - val application = activityRule.activity.application as? TestAppManager - application?.workerFactory = workerFactory - } - // A no-op interaction to afford us an easy, harmless way to get a11y checking to trigger. fun meaninglessSwipe() { Espresso.onView(ViewMatchers.withId(R.id.action_bar_root)).swipeRight() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/ViewUtils.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/ViewUtils.kt deleted file mode 100644 index 80e0d0a305..0000000000 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/ViewUtils.kt +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright (C) 2018-present Instructure, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -package com.instructure.student.ui.utils - -import android.view.View -import androidx.test.espresso.Espresso -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.assertion.ViewAssertions -import org.hamcrest.Matcher - -object ViewUtils { - - fun pressBackButton(times: Int) { - for(i in 1..times) { - Espresso.pressBack() - } - } - - fun waitForViewToDisappear(viewMatcher: Matcher, timeoutInSeconds: Long) { - val startTime = System.currentTimeMillis() - - while (System.currentTimeMillis() - startTime < (timeoutInSeconds * 1000)) { - try { - onView(viewMatcher) - .check(ViewAssertions.doesNotExist()) - return - } catch (e: AssertionError) { - Thread.sleep(200) - } - } - throw AssertionError("The view has not been displayed within $timeoutInSeconds seconds.") - } -} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/extensions/StudentTestExtensions.kt similarity index 99% rename from apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/utils/extensions/StudentTestExtensions.kt index e98329a47f..1f74691925 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/extensions/StudentTestExtensions.kt @@ -16,7 +16,7 @@ */ @file:Suppress("unused") -package com.instructure.student.ui.utils +package com.instructure.student.ui.utils.extensions import android.app.Activity import android.content.Intent @@ -54,6 +54,7 @@ import com.instructure.interactions.router.Route import com.instructure.student.R import com.instructure.student.activity.LoginActivity import com.instructure.student.router.RouteMatcher +import com.instructure.student.ui.utils.StudentTest import java.io.File import java.io.FileWriter @@ -342,4 +343,4 @@ fun uploadTextFile( token, fileUploadType ) -} +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/offline/OfflineTestUtils.kt similarity index 98% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/utils/offline/OfflineTestUtils.kt index 9ad81df4b4..ae58424527 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/offline/OfflineTestUtils.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.e2e.offline.utils +package com.instructure.student.ui.utils.offline import androidx.test.espresso.Espresso.onView import androidx.test.espresso.matcher.ViewMatchers.hasDescendant diff --git a/apps/student/src/main/AndroidManifest.xml b/apps/student/src/main/AndroidManifest.xml index 6cc7ffea16..af986c7a9d 100644 --- a/apps/student/src/main/AndroidManifest.xml +++ b/apps/student/src/main/AndroidManifest.xml @@ -286,12 +286,6 @@ - - diff --git a/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt index 7f61d016b4..54d92ece0f 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt @@ -317,7 +317,7 @@ abstract class BaseRouterActivity : CallbackActivity(), FullScreenInteractions { } private suspend fun shouldOpenInternally(url: String): Boolean { - val mediaUrl = RouteUtils.getRedirectUrl(Uri.parse(url)).toString() + val mediaUrl = RouteUtils.getMediaUri(Uri.parse(url)).toString() return (mediaUrl.endsWith(".mpd") || mediaUrl.endsWith(".m3u8") || mediaUrl.endsWith(".mp4")) } diff --git a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt index 2f9e4a8010..94d384b33d 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt @@ -89,7 +89,6 @@ import com.instructure.pandautils.features.notification.preferences.PushNotifica import com.instructure.pandautils.features.offline.sync.OfflineSyncHelper import com.instructure.pandautils.features.reminder.AlarmScheduler import com.instructure.pandautils.features.settings.SettingsFragment -import com.instructure.pandautils.features.themeselector.ThemeSelectorBottomSheet import com.instructure.pandautils.interfaces.NavigationCallbacks import com.instructure.pandautils.models.PushNotification import com.instructure.pandautils.receivers.PushExternalReceiver @@ -385,12 +384,6 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. val savedBottomScreens = savedInstanceState?.getStringArrayList(BOTTOM_SCREENS_BUNDLE_KEY) restoreBottomNavState(savedBottomScreens) - if (!ThemePrefs.themeSelectionShown) { - val themeSelector = ThemeSelectorBottomSheet() - themeSelector.show(supportFragmentManager, ThemeSelectorBottomSheet::javaClass.name) - ThemePrefs.themeSelectionShown = true - } - requestNotificationsPermission() networkStateProvider.isOnlineLiveData.observe(this) { isOnline -> diff --git a/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt index 37dd826bce..bf58280176 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt @@ -86,7 +86,7 @@ class VideoViewActivity : BaseCanvasActivity() { private fun fetchMediaUri(uri: Uri) { lifecycleScope.launch { - val mediaUri = RouteUtils.getRedirectUrl(uri) + val mediaUri = RouteUtils.getMediaUri(uri) player = ExoPlayer.Builder(this@VideoViewActivity) .setTrackSelector(trackSelector) .setLoadControl(DefaultLoadControl()) @@ -100,7 +100,7 @@ class VideoViewActivity : BaseCanvasActivity() { private fun buildMediaSource(uri: Uri): MediaSource { val mediaItem = MediaItem.fromUri(uri) - return when (val type = Util.inferContentType(uri.lastPathSegment ?: "")) { + return when (val type = Util.inferContentType(uri)) { C.CONTENT_TYPE_SS -> SsMediaSource.Factory(DefaultSsChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false)).createMediaSource(mediaItem) C.CONTENT_TYPE_DASH -> DashMediaSource.Factory(DefaultDashChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false)).createMediaSource(mediaItem) C.CONTENT_TYPE_HLS -> HlsMediaSource.Factory(DefaultHlsDataSourceFactory(buildDataSourceFactory(false))).createMediaSource(mediaItem) diff --git a/apps/student/src/main/java/com/instructure/student/adapter/NotificationListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/NotificationListRecyclerAdapter.kt index a6f52bc80b..537074498e 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/NotificationListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/NotificationListRecyclerAdapter.kt @@ -24,31 +24,27 @@ import com.instructure.canvasapi2.managers.CourseManager.createCourseMap import com.instructure.canvasapi2.managers.CourseManager.getCoursesWithGradingScheme import com.instructure.canvasapi2.managers.GroupManager.createGroupMap import com.instructure.canvasapi2.managers.GroupManager.getAllGroups -import com.instructure.canvasapi2.managers.InboxManager.getConversation import com.instructure.canvasapi2.managers.StreamManager.getCourseStream import com.instructure.canvasapi2.managers.StreamManager.getUserStream import com.instructure.canvasapi2.managers.StreamManager.hideStreamItem import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.models.HiddenStreamItem import com.instructure.canvasapi2.models.StreamItem import com.instructure.canvasapi2.utils.APIHelper.hasNetworkConnection -import com.instructure.canvasapi2.utils.ApiPrefs.user import com.instructure.canvasapi2.utils.ApiType import com.instructure.canvasapi2.utils.DateHelper import com.instructure.canvasapi2.utils.LinkHeaders import com.instructure.pandarecycler.util.GroupSortedList.GroupComparatorCallback import com.instructure.pandarecycler.util.GroupSortedList.ItemComparatorCallback import com.instructure.pandarecycler.util.Types -import com.instructure.student.R import com.instructure.student.holders.ExpandableViewHolder import com.instructure.student.holders.NotificationViewHolder import com.instructure.student.interfaces.NotificationAdapterToFragmentCallback import retrofit2.Call import retrofit2.Response -import java.util.* +import java.util.Date class NotificationListRecyclerAdapter( context: Context, @@ -271,47 +267,10 @@ class NotificationListRecyclerAdapter( // Wait until all calls return; if (courseMap == null || groupMap == null || streamItems == null) return for (streamItem in streamItems!!) { - streamItem.setCanvasContextFromMap(courseMap!!, groupMap!!) + // Skip conversation type items from notification list. + if (streamItem.getStreamItemType() === StreamItem.Type.CONVERSATION) continue - // Load conversations if needed - if (streamItem.getStreamItemType() === StreamItem.Type.CONVERSATION && user != null) { - getConversation( - streamItem.conversationId, - false, - object : StatusCallback() { - override fun onResponse( - response: Response, - linkHeaders: LinkHeaders, - type: ApiType - ) { - // Need to make sure the user isn't null - if (user != null) { - streamItem.setConversation( - context, - response.body(), - user!!.id, - context.getString(R.string.monologue) - ) - notifyDataSetChanged() - } - } - - override fun onFail(call: Call?, error: Throwable, response: Response<*>?) { - // Show crouton if it's a network error - if (!hasNetworkConnection()) { - adapterToFragmentCallback.onShowErrorCrouton(R.string.noDataConnection) - } else if (user != null) { - val conversation = Conversation() - conversation.isDeleted = true - conversation.deletedString = context.getString(R.string.deleted) - streamItem.setConversation(context, conversation, user!!.id, context.getString(R.string.monologue)) - notifyDataSetChanged() - } - } - }, - false - ) - } + streamItem.setCanvasContextFromMap(courseMap!!, groupMap!!) // Make sure there's something there if (streamItem.updatedDate == null) continue diff --git a/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt index 6162f659a2..0106b2fcc4 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt @@ -242,10 +242,11 @@ open class TodoListRecyclerAdapter : ExpandableRecyclerAdapter - try { - val whatIfText = currentScoreView?.text.toString() - callback(if(whatIfText.isBlank()) null else whatIfText.toDouble(), assignment.pointsPossible) - } catch (e: Throwable) { - callback(null, assignment.pointsPossible) - } - dismissAllowingStateLoss() - } + .setPositiveButton(R.string.done, null) .setNegativeButton(R.string.cancel) { _, _ -> dismissAllowingStateLoss() } @SuppressLint("InflateParams") @@ -73,6 +66,26 @@ class WhatIfDialogStyled : BaseCanvasDialogFragment() { dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(textButtonColor) dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(textButtonColor) } + + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + try { + val whatIfText = currentScoreView?.text.toString() + if (whatIfText.isNotBlank()) { + val enteredScore = whatIfText.toDouble() + if (enteredScore > assignment.pointsPossible) { + toast(R.string.whatIfScoreExceedsMaximum) + return@setOnClickListener + } + callback(enteredScore, assignment.pointsPossible) + } else { + callback(null, assignment.pointsPossible) + } + dismissAllowingStateLoss() + } catch (e: Throwable) { + callback(null, assignment.pointsPossible) + dismissAllowingStateLoss() + } + } } dialog.setCanceledOnTouchOutside(true) diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsBehaviour.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsBehaviour.kt index 2e5b61a000..0bd0bfcfdb 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsBehaviour.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsBehaviour.kt @@ -24,6 +24,7 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar import androidx.fragment.app.FragmentActivity +import android.os.Bundle import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course @@ -31,6 +32,7 @@ import com.instructure.canvasapi2.models.LTITool import com.instructure.canvasapi2.utils.APIHelper import com.instructure.canvasapi2.utils.Analytics import com.instructure.canvasapi2.utils.AnalyticsEventConstants +import com.instructure.canvasapi2.utils.AnalyticsParamConstants import com.instructure.interactions.Navigation import com.instructure.interactions.bookmarks.Bookmarker import com.instructure.pandautils.databinding.FragmentAssignmentDetailsBinding @@ -64,7 +66,6 @@ class StudentAssignmentDetailsBehaviour ( startVideoCapture: () -> Unit, onLaunchMediaPicker: () -> Unit, ) { - Analytics.logEvent(AnalyticsEventConstants.SUBMIT_MEDIARECORDING_SELECTED) val builder = AlertDialog.Builder(activity) val dialogBinding = DialogSubmissionPickerMediaBinding.inflate(LayoutInflater.from(activity)) val dialog = builder.setView(dialogBinding.root).create() @@ -108,38 +109,55 @@ class StudentAssignmentDetailsBehaviour ( isStudioEnabled: Boolean, studioLTITool: LTITool? ) { + Analytics.logEvent(AnalyticsEventConstants.ASSIGNMENT_SUBMIT_SELECTED) + val builder = AlertDialog.Builder(activity) val dialogBinding = DialogSubmissionPickerBinding.inflate(LayoutInflater.from(activity)) val dialog = builder.setView(dialogBinding.root).create() val submissionTypes = assignment.getSubmissionTypes() dialog.setOnShowListener { + val nextAttempt = (assignment.submission?.attempt ?: 0) + 1 setupDialogRow(dialog, dialogBinding.submissionEntryText, submissionTypes.contains( Assignment.SubmissionType.ONLINE_TEXT_ENTRY)) { + Analytics.logEvent(AnalyticsEventConstants.SUBMIT_TEXTENTRY_SELECTED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString()) + }) router.navigateToTextEntryScreen( activity, course, assignment.id, assignment.name.orEmpty(), + attempt = nextAttempt.toLong() ) } setupDialogRow(dialog, dialogBinding.submissionEntryWebsite, submissionTypes.contains( Assignment.SubmissionType.ONLINE_URL)) { + Analytics.logEvent(AnalyticsEventConstants.SUBMIT_URL_SELECTED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString()) + }) router.navigateToUrlSubmissionScreen( activity, course, assignment.id, assignment.name.orEmpty(), null, - false + false, + nextAttempt.toLong() ) } setupDialogRow(dialog, dialogBinding.submissionEntryFile, submissionTypes.contains( Assignment.SubmissionType.ONLINE_UPLOAD)) { - router.navigateToUploadScreen(activity, course, assignment) + Analytics.logEvent(AnalyticsEventConstants.SUBMIT_FILEUPLOAD_SELECTED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString()) + }) + router.navigateToUploadScreen(activity, course, assignment, attempt = nextAttempt.toLong()) } setupDialogRow(dialog, dialogBinding.submissionEntryMedia, submissionTypes.contains( Assignment.SubmissionType.MEDIA_RECORDING)) { + Analytics.logEvent(AnalyticsEventConstants.SUBMIT_MEDIARECORDING_SELECTED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString()) + }) showMediaDialog(activity, binding, recordCallback, startVideoCapture, onLaunchMediaPicker) } setupDialogRow( @@ -147,10 +165,16 @@ class StudentAssignmentDetailsBehaviour ( dialogBinding.submissionEntryStudio, isStudioEnabled ) { + Analytics.logEvent(AnalyticsEventConstants.SUBMIT_STUDIO_SELECTED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString()) + }) navigateToStudioScreen(activity, course, assignment, studioLTITool) } setupDialogRow(dialog, dialogBinding.submissionEntryStudentAnnotation, submissionTypes.contains( Assignment.SubmissionType.STUDENT_ANNOTATION)) { + Analytics.logEvent(AnalyticsEventConstants.SUBMIT_ANNOTATION_SELECTED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString()) + }) assignment.submission?.id?.let{ router.navigateToAnnotationSubmissionScreen( activity, @@ -158,7 +182,8 @@ class StudentAssignmentDetailsBehaviour ( assignment.annotatableAttachmentId, it, assignment.id, - assignment.name.orEmpty()) + assignment.name.orEmpty(), + nextAttempt.toLong()) } } } @@ -174,7 +199,7 @@ class StudentAssignmentDetailsBehaviour ( } private fun navigateToStudioScreen(activity: FragmentActivity, canvasContext: CanvasContext, assignment: Assignment, studioLTITool: LTITool?) { - Analytics.logEvent(AnalyticsEventConstants.SUBMIT_STUDIO_SELECTED) + val nextAttempt = (assignment.submission?.attempt ?: 0) + 1 RouteMatcher.route( activity, StudioWebViewFragment.makeRoute( @@ -182,7 +207,8 @@ class StudentAssignmentDetailsBehaviour ( studioLTITool?.getResourceSelectorUrl(canvasContext, assignment).orEmpty(), studioLTITool?.name.orEmpty(), true, - assignment + assignment, + nextAttempt.toLong() ) ) } diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt index 085ee40b20..5774c1e3af 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt @@ -23,8 +23,6 @@ import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.LTITool import com.instructure.canvasapi2.models.Quiz import com.instructure.canvasapi2.models.RemoteFile -import com.instructure.canvasapi2.utils.Analytics -import com.instructure.canvasapi2.utils.AnalyticsEventConstants import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRouter import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment import com.instructure.pandautils.features.lti.LtiLaunchFragment @@ -44,11 +42,13 @@ class StudentAssignmentDetailsRouter: AssignmentDetailsRouter() { activity: FragmentActivity, canvasContext: CanvasContext, assignment: Assignment, - mediaUri: Uri + mediaUri: Uri, + attempt: Long, + mediaSource: String? ) { RouteMatcher.route( activity, - PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, mediaUri) + PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, mediaUri, attempt, mediaSource) ) } @@ -59,7 +59,8 @@ class StudentAssignmentDetailsRouter: AssignmentDetailsRouter() { assignmentUrl: String?, isAssignmentEnhancementEnabled: Boolean, isObserver: Boolean, - initialSelectedSubmissionAttempt: Long? + initialSelectedSubmissionAttempt: Long?, + isQuiz: Boolean ) { RouteMatcher.route( activity, @@ -89,12 +90,12 @@ class StudentAssignmentDetailsRouter: AssignmentDetailsRouter() { activity: FragmentActivity, canvasContext: CanvasContext, assignment: Assignment, - attemptId: Long? + attemptId: Long?, + attempt: Long ) { - Analytics.logEvent(AnalyticsEventConstants.SUBMIT_FILEUPLOAD_SELECTED) RouteMatcher.route( activity, - PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, PickerSubmissionMode.FileSubmission) + PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, PickerSubmissionMode.FileSubmission, attempt) ) } @@ -104,12 +105,12 @@ class StudentAssignmentDetailsRouter: AssignmentDetailsRouter() { assignmentId: Long, assignmentName: String?, initialText: String?, - isFailure: Boolean + isFailure: Boolean, + attempt: Long ) { - Analytics.logEvent(AnalyticsEventConstants.SUBMIT_TEXTENTRY_SELECTED) RouteMatcher.route( activity, - TextSubmissionUploadFragment.makeRoute(course, assignmentId, assignmentName, initialText, isFailure) + TextSubmissionUploadFragment.makeRoute(course, assignmentId, assignmentName, initialText, isFailure, attempt) ) } @@ -119,12 +120,12 @@ class StudentAssignmentDetailsRouter: AssignmentDetailsRouter() { assignmentId: Long, assignmentName: String?, initialUrl: String?, - isFailure: Boolean + isFailure: Boolean, + attempt: Long ) { - Analytics.logEvent(AnalyticsEventConstants.SUBMIT_ONLINEURL_SELECTED) RouteMatcher.route( activity, - UrlSubmissionUploadFragment.makeRoute(course, assignmentId, assignmentName, initialUrl, isFailure) + UrlSubmissionUploadFragment.makeRoute(course, assignmentId, assignmentName, initialUrl, isFailure, attempt) ) } @@ -132,11 +133,11 @@ class StudentAssignmentDetailsRouter: AssignmentDetailsRouter() { activity: FragmentActivity, canvasContext: CanvasContext, annotatableAttachmentId: Long, - submissionId: Long, + submissionId: Long, assignmentId: Long, - assignmentName: String + assignmentName: String, + attempt: Long ) { - Analytics.logEvent(AnalyticsEventConstants.SUBMIT_STUDENT_ANNOTATION_SELECTED) RouteMatcher.route( activity, AnnotationSubmissionUploadFragment.makeRoute( @@ -144,7 +145,8 @@ class StudentAssignmentDetailsRouter: AssignmentDetailsRouter() { annotatableAttachmentId, submissionId, assignmentId, - assignmentName + assignmentName, + attempt ) ) } diff --git a/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRepository.kt b/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRepository.kt index 32f6d25237..b29ed0a0df 100644 --- a/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRepository.kt +++ b/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRepository.kt @@ -26,7 +26,6 @@ import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.depaginate import com.instructure.canvasapi2.utils.hasActiveEnrollment -import com.instructure.canvasapi2.utils.isValidTerm import com.instructure.pandautils.features.calendar.CalendarRepository import com.instructure.pandautils.room.calendar.daos.CalendarFilterDao import com.instructure.pandautils.room.calendar.entities.CalendarFilterEntity @@ -54,6 +53,7 @@ class StudentCalendarRepository( startDate, endDate, emptyList(), // We always request all the events for students and filter locally + null, restParams ).depaginate { plannerApi.nextPagePlannerItems(it, restParams) diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt index 29fcb4a45a..5ce5601cb8 100644 --- a/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt @@ -69,6 +69,7 @@ import com.instructure.pandautils.features.discussion.details.DiscussionDetailsW import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.utils.BooleanArg import com.instructure.pandautils.utils.DiscussionEntryEvent +import com.instructure.pandautils.utils.FeatureFlagProvider import com.instructure.pandautils.utils.LongArg import com.instructure.pandautils.utils.NetworkStateProvider import com.instructure.pandautils.utils.NullableParcelableArg @@ -131,6 +132,9 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { @Inject lateinit var networkStateProvider: NetworkStateProvider + @Inject + lateinit var featureFlagProvider: FeatureFlagProvider + // Bundle args @get:PageViewUrlParam("canvasContext") var canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) @@ -782,11 +786,11 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { replyToDiscussionTopic.setVisible(discussionTopicHeader.permissions?.reply ?: false) replyToDiscussionTopic.onClick { showReplyView(discussionTopicHeader.id) } - discussionTopicHeaderWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), discussionTopicHeader.message, { + discussionTopicHeaderWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), featureFlagProvider, discussionTopicHeader.message, { if (view != null) loadHTMLTopic(it, discussionTopicHeader.title) }, onLtiButtonPressed = { RouteMatcher.route(requireActivity(), LtiLaunchFragment.makeSessionlessLtiUrlRoute(requireActivity(), canvasContext, it)) - }) + }, courseId = canvasContext.id) attachmentIcon.setVisible(discussionTopicHeader.attachments.isNotEmpty()) attachmentIcon.onClick { @@ -807,9 +811,31 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { setupRepliesWebView() - discussionRepliesWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), html, { formattedHtml -> - discussionRepliesWebViewWrapper.loadDataWithBaseUrl(CanvasWebView.getReferrer(true), formattedHtml, "text/html", "UTF-8", null) - }, onLtiButtonPressed = { RouteMatcher.route(requireActivity(), LtiLaunchFragment.makeSessionlessLtiUrlRoute(requireActivity(), canvasContext, it)) }) + discussionRepliesWebViewWrapper.webView.loadHtmlWithIframes( + requireContext(), + featureFlagProvider, + html, + { formattedHtml -> + discussionRepliesWebViewWrapper.loadDataWithBaseUrl( + CanvasWebView.getReferrer(true), + formattedHtml, + "text/html", + "UTF-8", + null + ) + }, + onLtiButtonPressed = { + RouteMatcher.route( + requireActivity(), + LtiLaunchFragment.makeSessionlessLtiUrlRoute( + requireActivity(), + canvasContext, + it + ) + ) + }, + courseId = canvasContext.id + ) swipeRefreshLayout.isRefreshing = false discussionTopicRepliesTitle.setVisible(discussionTopicHeader.shouldShowReplies) diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListHolder.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListHolder.kt index e9673e4bdf..60616980af 100644 --- a/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListHolder.kt @@ -20,13 +20,16 @@ import android.content.Context import android.view.View import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView +import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.utils.DateHelper import com.instructure.canvasapi2.utils.localized import com.instructure.pandautils.utils.onClick +import com.instructure.pandautils.utils.orderedCheckpoints import com.instructure.pandautils.utils.setGone import com.instructure.pandautils.utils.setInvisible import com.instructure.pandautils.utils.setVisible +import com.instructure.pandautils.utils.toFormattedString import com.instructure.student.R import com.instructure.student.databinding.ViewholderDiscussionBinding import java.util.Date @@ -82,6 +85,9 @@ class DiscussionListHolder(view: View) : RecyclerView.ViewHolder(view) { discussionIcon.hideNestedIcon() } + checkpointDueDates.text = getFormattedCheckpointDates(context, discussionTopicHeader.assignment) + checkpointDueDates.setVisible(checkpointDueDates.text.isNotEmpty()) + dueDate.text = when { isAssignmentType -> { if (discussionTopicHeader.assignment!!.dueDate == null) getFormattedLastPost( @@ -106,17 +112,26 @@ class DiscussionListHolder(view: View) : RecyclerView.ViewHolder(view) { statusIndicator.setInvisible() } - val entryCountString = - context.resources.getQuantityString(R.plurals.utils_discussionsReplies, entryCount, entryCount.localized) + val entryCountString = context.resources.getQuantityString( + R.plurals.utils_discussionsReplies, + entryCount, + entryCount.localized + ) val unreadCountString = context.resources.getQuantityString( R.plurals.utils_discussionsUnread, discussionTopicHeader.unreadCount, unreadDisplayCount ) + val pointsText = discussionTopicHeader.assignment?.pointsPossible?.toInt()?.let { + context.resources.getQuantityString( + R.plurals.quantityPointsAbbreviated, + it, + it + ) + } - readUnreadCounts.text = context.getString( - R.string.utils_discussionsUnreadRepliesBlank, - entryCountString, context.getString(R.string.utils_dotWithSpaces), unreadCountString + readUnreadCounts.text = listOfNotNull(pointsText, entryCountString, unreadCountString).joinToString( + context.getString(R.string.utils_dotWithSpaces) ) } @@ -137,6 +152,19 @@ class DiscussionListHolder(view: View) : RecyclerView.ViewHolder(view) { return context.getString(R.string.utils_dueDateAtTime).format(dueDate, dueTime) } + private fun getFormattedCheckpointDates(context: Context, assignment: Assignment?): String { + return assignment?.orderedCheckpoints.orEmpty().joinToString("\n") { checkpoint -> + getDueDateText(context, checkpoint.dueDate) + } + } + + private fun getDueDateText(context: Context, dueDate: Date?): String { + return when { + dueDate == null -> context.getString(R.string.noDueDate) + else -> context.getString(R.string.due, dueDate.toFormattedString()) + } + } + companion object { const val HOLDER_RES_ID = R.layout.viewholder_discussion } diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/routing/StudentDiscussionRouter.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/routing/StudentDiscussionRouter.kt index 4b273b80ff..c2d1206df3 100644 --- a/apps/student/src/main/java/com/instructure/student/features/discussion/routing/StudentDiscussionRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/routing/StudentDiscussionRouter.kt @@ -45,5 +45,14 @@ class StudentDiscussionRouter( RouteMatcher.route(fragmentActivity, route) } + override fun routeToDiscussionWebView(canvasContext: CanvasContext, discussionTopicHeaderId: Long) { + val route = DiscussionDetailsWebViewFragment.makeRoute(canvasContext, discussionTopicHeaderId) + route.apply { + removePreviousScreen = true + } + + RouteMatcher.route(fragmentActivity, route) + } + override fun routeToNativeSpeedGrader(courseId: Long, assignmentId: Long, submissionIds: List, selectedIdx: Int, anonymousGrading: Boolean?, discussionTopicEntryId: Long?) = Unit } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/documentscanning/BitmapExtensions.kt b/apps/student/src/main/java/com/instructure/student/features/documentscanning/BitmapExtensions.kt deleted file mode 100644 index a4a8cd2a98..0000000000 --- a/apps/student/src/main/java/com/instructure/student/features/documentscanning/BitmapExtensions.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2022 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.instructure.student.features.documentscanning - -import android.graphics.* - -fun Bitmap.toGrayscale(): Bitmap { - val width = this.width - val height = this.height - - val grayscaleBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(grayscaleBitmap) - val paint = Paint() - val colorMatrix = ColorMatrix() - colorMatrix.setSaturation(0f) - val colorMatrixFilter = ColorMatrixColorFilter(colorMatrix) - paint.colorFilter = colorMatrixFilter - canvas.drawBitmap(this, 0f, 0f, paint) - return grayscaleBitmap -} - -fun Bitmap.toMonochrome(): Bitmap { - val width = this.width - val height = this.height - - val monochromeBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - - val hsv = FloatArray(3) - for (column in 0 until width) { - for (row in 0 until height) { - Color.colorToHSV(this.getPixel(column, row), hsv) - if (hsv[2] > 0.5f) { - monochromeBitmap.setPixel(column, row, Color.WHITE) - } else { - monochromeBitmap.setPixel(column, row, Color.BLACK) - } - } - } - - return monochromeBitmap -} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningActivity.kt b/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningActivity.kt deleted file mode 100644 index 73badd284b..0000000000 --- a/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningActivity.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2022 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.instructure.student.features.documentscanning - -import android.app.Activity -import android.content.Intent -import android.graphics.Bitmap -import android.os.Bundle -import androidx.activity.viewModels -import androidx.core.content.ContextCompat -import androidx.core.net.toUri -import androidx.databinding.DataBindingUtil -import com.instructure.pandautils.binding.viewBinding -import com.instructure.pandautils.utils.ViewStyler -import com.instructure.student.R -import com.instructure.student.databinding.ActivityDocumentScanningBinding -import com.zynksoftware.documentscanner.ScanActivity -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel -import com.zynksoftware.documentscanner.model.ScannerResults -import dagger.hilt.android.AndroidEntryPoint -import java.io.File -import java.io.FileOutputStream -import java.text.SimpleDateFormat -import java.util.* - -@AndroidEntryPoint -class DocumentScanningActivity : ScanActivity() { - - private lateinit var binding: ActivityDocumentScanningBinding - - private val viewModel: DocumentScanningViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = DataBindingUtil.setContentView(this, R.layout.activity_document_scanning) - binding.lifecycleOwner = this - binding.viewModel = viewModel - - addFragmentContentLayout() - - setupToolbar() - - viewModel.events.observe(this) { event -> - event.getContentIfNotHandled()?.let { - handleAction(it) - } - } - } - - private fun handleAction(action: DocumentScanningAction) { - when (action) { - is DocumentScanningAction.SaveBitmapAction -> { - val file = File(filesDir, "scanned_${SimpleDateFormat("yyyyMMddkkmmss", Locale.getDefault()).format(Date())}.jpg") - var fileOutputStream: FileOutputStream? = null - try { - fileOutputStream = FileOutputStream(file.absolutePath) - action.bitmap.compress(Bitmap.CompressFormat.JPEG, action.quality, fileOutputStream) - val intent = Intent() - intent.data = file.toUri() - setResult(Activity.RESULT_OK, intent) - finish() - } finally { - fileOutputStream?.run { - flush() - close() - } - } - } - } - } - - override fun onClose() { - setResult(RESULT_CANCELED) - finish() - } - - override fun onError(error: DocumentScannerErrorModel) { - - } - - override fun onSuccess(scannerResults: ScannerResults) { - viewModel.setScannerResults(scannerResults) - } - - private fun setupToolbar() { - binding.toolbar.apply { - setTitle(R.string.documentScanningTitle) - navigationIcon = ContextCompat.getDrawable(this@DocumentScanningActivity, R.drawable.ic_back_arrow) - navigationIcon?.isAutoMirrored = true - ViewStyler.themeToolbarLight(this@DocumentScanningActivity, this) - setNavigationContentDescription(R.string.close) - setNavigationOnClickListener { onClose() } - } - } -} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningViewData.kt b/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningViewData.kt deleted file mode 100644 index 7db75cde27..0000000000 --- a/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningViewData.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2022 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.instructure.student.features.documentscanning - -import android.graphics.Bitmap -import androidx.databinding.BaseObservable -import androidx.databinding.Bindable -import com.instructure.student.features.documentscanning.itemviewmodels.FilterItemViewModel - -data class DocumentScanningViewData( - @get:Bindable var selectedBitmap: Bitmap, - val filterItemViewModels: List -) : BaseObservable() - -data class FilterItemViewData( - val bitmap: Bitmap, - val name: String -) - -sealed class DocumentScanningAction { - data class SaveBitmapAction(val bitmap: Bitmap, val quality: Int): DocumentScanningAction() -} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningViewModel.kt deleted file mode 100644 index 7a99039541..0000000000 --- a/apps/student/src/main/java/com/instructure/student/features/documentscanning/DocumentScanningViewModel.kt +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (C) 2022 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.instructure.student.features.documentscanning - -import android.content.res.Resources -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import com.instructure.pandautils.R -import com.instructure.pandautils.BR -import com.instructure.student.features.documentscanning.itemviewmodels.FilterItemViewModel -import com.instructure.pandautils.mvvm.Event -import com.instructure.pandautils.mvvm.ViewState -import com.zynksoftware.documentscanner.model.ScannerResults -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -@HiltViewModel -class DocumentScanningViewModel @Inject constructor( - private val resources: Resources -) : ViewModel() { - - val state: LiveData - get() = _state - private val _state = MutableLiveData() - - val data: LiveData - get() = _data - private val _data = MutableLiveData() - - val events: LiveData> - get() = _events - private val _events = MutableLiveData>() - - private lateinit var selectedItem: FilterItemViewModel - - fun setScannerResults(results: ScannerResults) { - _state.postValue(ViewState.Loading) - createViewData(results) - } - - private fun createViewData(results: ScannerResults) { - if (results.croppedImageFile != null && results.originalImageFile != null) { - val croppedBitmap = BitmapFactory.decodeFile(results.croppedImageFile!!.path) - val originalBitmap = BitmapFactory.decodeFile(results.originalImageFile!!.path) - val grayscaleBitmap = croppedBitmap.toGrayscale() - val monochromeBitmap = croppedBitmap.toMonochrome() - - //We no longer need these files - results.croppedImageFile?.delete() - results.croppedImageFile?.delete() - results.transformedImageFile?.delete() - - val filters = listOf( - createFilterViewModel(croppedBitmap, true, resources.getString(R.string.filter_name_color)), - createFilterViewModel(grayscaleBitmap, false, resources.getString(R.string.filter_name_grayscale)), - createFilterViewModel(monochromeBitmap, false, resources.getString(R.string.filter_name_monochrome)), - createFilterViewModel(originalBitmap, false, resources.getString(R.string.filter_name_original)) - ) - selectedItem = filters[0] - - val viewData = DocumentScanningViewData( - croppedBitmap, - filters - ) - _data.postValue(viewData) - _state.postValue(ViewState.Success) - } else { - _state.postValue(ViewState.Error()) - } - } - - private fun createFilterViewModel(bitmap: Bitmap, selected: Boolean, name: String): FilterItemViewModel { - return FilterItemViewModel( - FilterItemViewData(bitmap, name), - selected, - this::onFilterSelected - ) - } - - fun onFilterSelected(itemViewModel: FilterItemViewModel) { - selectedItem.apply { - selected = false - notifyPropertyChanged(BR.selected) - } - _data.value?.apply { - selectedBitmap = itemViewModel.data.bitmap - notifyPropertyChanged(BR.selectedBitmap) - } - selectedItem = itemViewModel - } - - fun onSaveClicked() { - _events.postValue(Event(DocumentScanningAction.SaveBitmapAction(selectedItem.data.bitmap, 100))) - } -} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/documentscanning/itemviewmodels/FilterItemViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/documentscanning/itemviewmodels/FilterItemViewModel.kt deleted file mode 100644 index c84895a93e..0000000000 --- a/apps/student/src/main/java/com/instructure/student/features/documentscanning/itemviewmodels/FilterItemViewModel.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2022 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.instructure.student.features.documentscanning.itemviewmodels - -import androidx.databinding.BaseObservable -import androidx.databinding.Bindable -import com.instructure.pandautils.BR -import com.instructure.student.features.documentscanning.FilterItemViewData -import com.instructure.pandautils.mvvm.ItemViewModel -import com.instructure.student.R - -class FilterItemViewModel( - val data: FilterItemViewData, - @get:Bindable var selected: Boolean, - val onSelect: (FilterItemViewModel) -> Unit -) : ItemViewModel, BaseObservable() { - override val layoutId: Int = R.layout.item_document_scanning_filter - - fun select() { - if (!selected) { - selected = true - notifyPropertyChanged(BR.selected) - onSelect(this) - } - } -} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt index df6e22f475..c6fd5a2a59 100644 --- a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt @@ -551,8 +551,9 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent } } - override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { + override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { workInfoLiveData.observe(viewLifecycleOwner) { + if (it == null) return@observe if (it.state == WorkInfo.State.SUCCEEDED) { updateFileList(true) folder?.let { fileFolder -> diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListRepository.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListRepository.kt index 4dbfd3dc89..e6149d52e9 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListRepository.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListRepository.kt @@ -16,6 +16,7 @@ */ package com.instructure.student.features.modules.list +import com.instructure.canvasapi2.managers.graphql.ModuleItemWithCheckpoints import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.ModuleItem @@ -23,6 +24,8 @@ import com.instructure.canvasapi2.models.ModuleObject import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.utils.DataResult import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.Const.REPLY_TO_ENTRY +import com.instructure.pandautils.utils.Const.REPLY_TO_TOPIC import com.instructure.pandautils.utils.FeatureFlagProvider import com.instructure.pandautils.utils.NetworkStateProvider import com.instructure.student.features.modules.list.datasource.ModuleListDataSource @@ -74,4 +77,16 @@ class ModuleListRepository( suspend fun getNextPageModuleItems(nextUrl: String, forceNetwork: Boolean): DataResult> { return networkDataSource.getNextPageModuleItems(nextUrl, forceNetwork) } + + suspend fun getModuleItemCheckpoints(courseId: String, forceNetwork: Boolean): List { + return dataSource().getModuleItemCheckpoints(courseId, forceNetwork).map { + it.copy(checkpoints = it.checkpoints.sortedBy { checkpoint -> + when (checkpoint.tag) { + REPLY_TO_TOPIC -> 0 + REPLY_TO_ENTRY -> 1 + else -> 2 + } + }) + } + } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleListRecyclerAdapter.kt index de002820d8..78de33a3db 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleListRecyclerAdapter.kt @@ -27,6 +27,8 @@ import android.view.View import android.view.WindowManager import android.widget.ProgressBar import androidx.recyclerview.widget.RecyclerView +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.instructure.canvasapi2.managers.graphql.ModuleItemCheckpoint import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings @@ -63,16 +65,32 @@ open class ModuleListRecyclerAdapter( private val repository: ModuleListRepository, private val lifecycleScope: CoroutineScope, private val adapterToFragmentCallback: ModuleAdapterToFragmentCallback? -) : ExpandableRecyclerAdapter(context, ModuleObject::class.java, ModuleItem::class.java) { +) : ExpandableRecyclerAdapter( + context, + ModuleObject::class.java, + ModuleItem::class.java +) { private var initialDataJob: Job? = null private var moduleObjectJob: Job? = null private val moduleFromNetworkOrDb = HashMap() private var courseSettings: CourseSettings? = null + private var moduleItemCheckpointsMap: Map> = emptyMap() /* For testing purposes only */ - protected constructor(context: Context, repository: ModuleListRepository, lifecycleScope: CoroutineScope) : this(CanvasContext.defaultCanvasContext(), context, false, repository, lifecycleScope, null) // Callback not needed for testing, cast to null + protected constructor( + context: Context, + repository: ModuleListRepository, + lifecycleScope: CoroutineScope + ) : this( + CanvasContext.defaultCanvasContext(), + context, + false, + repository, + lifecycleScope, + null + ) // Callback not needed for testing, cast to null init { viewHolderHeaderClicked = object : ViewHolderHeaderClicked { @@ -113,9 +131,12 @@ open class ModuleListRecyclerAdapter( val courseColor = courseContext.color val groupItemCount = getGroupItemCount(moduleObject) val itemPosition = storedIndexOfItem(moduleObject, moduleItem) + val checkpoints = moduleItemCheckpointsMap[moduleItem.id.toString()] - (holder as ModuleViewHolder).bind(moduleObject, moduleItem, context, adapterToFragmentCallback, courseColor, - itemPosition == 0, itemPosition == groupItemCount - 1, courseSettings?.restrictQuantitativeData.orDefault()) + (holder as ModuleViewHolder).bind( + moduleObject, moduleItem, context, adapterToFragmentCallback, courseColor, + itemPosition == 0, itemPosition == groupItemCount - 1, courseSettings?.restrictQuantitativeData.orDefault(), checkpoints + ) } } @@ -151,6 +172,16 @@ open class ModuleListRecyclerAdapter( super.refresh() } + private suspend fun fetchModuleItemCheckpoints() { + try { + val checkpoints = repository.getModuleItemCheckpoints(courseContext.id.toString(), true) + moduleItemCheckpointsMap = checkpoints.associate { it.moduleItemId to it.checkpoints } + } catch (e: Exception) { + FirebaseCrashlytics.getInstance().recordException(e) + moduleItemCheckpointsMap = emptyMap() + } + } + // region Expandable Callbacks override fun createGroupCallback(): GroupSortedList.GroupComparatorCallback { return object : GroupSortedList.GroupComparatorCallback { @@ -198,7 +229,10 @@ open class ModuleListRecyclerAdapter( dialog.setContentView(R.layout.progress_dialog) val currentColor = courseContext.color - (dialog.findViewById(R.id.progressBar) as ProgressBar).indeterminateDrawable.setColorFilter(currentColor, PorterDuff.Mode.SRC_ATOP) + (dialog.findViewById(R.id.progressBar) as ProgressBar).indeterminateDrawable.setColorFilter( + currentColor, + PorterDuff.Mode.SRC_ATOP + ) return dialog } @@ -266,7 +300,8 @@ open class ModuleListRecyclerAdapter( if (failedResult.response != null && errorCode == 504 && APIHelper.isCachedResponse(failedResult.response!!) - && !Utils.isNetworkAvailable(context)) { + && !Utils.isNetworkAvailable(context) + ) { expandGroup(moduleObject, isNotifyGroupChange) } } @@ -324,7 +359,7 @@ open class ModuleListRecyclerAdapter( } } } - if(!shouldExhaustPagination || result.linkHeaders.nextUrl == null) { + if (!shouldExhaustPagination || result.linkHeaders.nextUrl == null) { // If we should exhaust pagination wait until we are done exhausting pagination adapterToFragmentCallback?.onRefreshFinished() } @@ -338,6 +373,7 @@ open class ModuleListRecyclerAdapter( initialDataJob = lifecycleScope.tryLaunch { val tabs = repository.getTabs(courseContext, isRefresh) courseSettings = repository.loadCourseSettings(courseContext.id, isRefresh) + fetchModuleItemCheckpoints() // We only want to show modules if its a course nav option OR set to as the homepage if (tabs.find { it.tabId == "modules" } != null || (courseContext as Course).homePage?.apiString == "modules") { @@ -376,9 +412,10 @@ open class ModuleListRecyclerAdapter( } if (moduleObject.state != null && - moduleObject.state == ModuleObject.State.Locked.apiString && - getGroupItemCount(moduleObject) > 0 && - getItem(moduleObject, 0)?.type == ModuleObject.State.UnlockRequirements.apiString) { + moduleObject.state == ModuleObject.State.Locked.apiString && + getGroupItemCount(moduleObject) > 0 && + getItem(moduleObject, 0)?.type == ModuleObject.State.UnlockRequirements.apiString + ) { val reqs = StringBuilder() val ids = moduleObject.prerequisiteIds diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleViewHolder.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleViewHolder.kt index 8282540c4b..0080b2ca27 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleViewHolder.kt @@ -5,6 +5,7 @@ import android.graphics.Typeface import android.view.View import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView +import com.instructure.canvasapi2.managers.graphql.ModuleItemCheckpoint import com.instructure.canvasapi2.models.ModuleItem import com.instructure.canvasapi2.models.ModuleObject import com.instructure.canvasapi2.utils.DateHelper @@ -13,7 +14,6 @@ import com.instructure.canvasapi2.utils.isValid import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.DP import com.instructure.pandautils.utils.setGone -import com.instructure.pandautils.utils.setInvisible import com.instructure.pandautils.utils.setTextForVisibility import com.instructure.pandautils.utils.setVisible import com.instructure.student.R @@ -33,7 +33,8 @@ class ModuleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { courseColor: Int, isFirstItem: Boolean, isLastItem: Boolean, - restrictQuantitativeData: Boolean + restrictQuantitativeData: Boolean, + checkpoints: List? ) = with(ViewholderModuleBinding.bind(itemView)) { val isLocked = ModuleUtility.isGroupLocked(moduleObject) @@ -135,41 +136,67 @@ class ModuleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { // Details val details = moduleItem.moduleDetails if (details != null) { - val hasDate: Boolean - val hasPoints: Boolean - if (details.dueDate != null) { - date.text = DateHelper.createPrefixedDateTimeString( - context, - R.string.toDoDue, - details.dueDate - ) - hasDate = true + // Handle checkpoints or regular due date + if (!checkpoints.isNullOrEmpty()) { + // Hide the single date field + date.setGone() + + // Clear previous checkpoint views + checkpointDatesContainer.removeAllViews() + checkpointDatesContainer.setVisible() + + // Create a TextView for each checkpoint + checkpoints.forEach { checkpoint -> + val checkpointDateView = android.widget.TextView(context).apply { + layoutParams = android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.MATCH_PARENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT + ) + setTextAppearance(R.style.AdapterItemDescriptionText) + text = if (checkpoint.dueAt != null) { + DateHelper.createPrefixedDateTimeString( + context, + R.string.toDoDue, + checkpoint.dueAt + ) + } else { + context.getString(R.string.toDoNoDueDate) + } + } + checkpointDatesContainer.addView(checkpointDateView) + } } else { - date.text = "" - hasDate = false + // Show single date field for regular module items + checkpointDatesContainer.setGone() + if (details.dueDate != null) { + date.text = DateHelper.createPrefixedDateTimeString( + context, + R.string.toDoDue, + details.dueDate + ) + date.setVisible() + } else { + date.text = "" + date.setGone() + } } + val pointsPossible = details.pointsPossible if (pointsPossible.isValid() && !restrictQuantitativeData) { points.text = context.getString( R.string.totalPoints, NumberHelper.formatDecimal(pointsPossible.toDouble(), 2, true) ) - hasPoints = true + points.setVisible() } else { points.text = "" - hasPoints = false - } - if (!hasDate && !hasPoints) { - date.setGone() points.setGone() - } else { - if (hasDate) date.setVisible() else date.setInvisible() - if (hasPoints) points.setVisible() else points.setInvisible() } } else { points.text = "" date.text = "" date.setGone() + checkpointDatesContainer.setGone() points.setGone() } BinderUtils.updateShadows(isFirstItem, isLastItem, shadowTop, shadowBottom) diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListDataSource.kt index 1826dea7f7..c55d9debe5 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListDataSource.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListDataSource.kt @@ -16,6 +16,7 @@ */ package com.instructure.student.features.modules.list.datasource +import com.instructure.canvasapi2.managers.graphql.ModuleItemWithCheckpoints import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.ModuleItem @@ -34,4 +35,6 @@ interface ModuleListDataSource { suspend fun getFirstPageModuleItems(canvasContext: CanvasContext, moduleId: Long, forceNetwork: Boolean): DataResult> suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? + + suspend fun getModuleItemCheckpoints(courseId: String, forceNetwork: Boolean): List } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSource.kt index 1a06c1c859..84940a4ca0 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSource.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSource.kt @@ -16,6 +16,7 @@ */ package com.instructure.student.features.modules.list.datasource +import com.instructure.canvasapi2.managers.graphql.ModuleItemWithCheckpoints import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.ModuleItem @@ -23,11 +24,18 @@ import com.instructure.canvasapi2.models.ModuleObject import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.utils.ApiType import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.daos.CheckpointDao import com.instructure.pandautils.room.offline.daos.CourseSettingsDao import com.instructure.pandautils.room.offline.daos.TabDao import com.instructure.pandautils.room.offline.facade.ModuleFacade +import com.instructure.pandautils.utils.orDefault -class ModuleListLocalDataSource(private val tabDao: TabDao, private val moduleFacade: ModuleFacade, private val courseSettingsDao: CourseSettingsDao) : ModuleListDataSource { +class ModuleListLocalDataSource( + private val tabDao: TabDao, + private val moduleFacade: ModuleFacade, + private val courseSettingsDao: CourseSettingsDao, + private val checkpointDao: CheckpointDao +) : ModuleListDataSource { override suspend fun getAllModuleObjects(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { val moduleObjects = moduleFacade.getModuleObjects(canvasContext.id) @@ -43,7 +51,11 @@ class ModuleListLocalDataSource(private val tabDao: TabDao, private val moduleFa return DataResult.Success(tabDao.findByCourseId(canvasContext.id).map { it.toApiModel() }, apiType = ApiType.DB) } - override suspend fun getFirstPageModuleItems(canvasContext: CanvasContext, moduleId: Long, forceNetwork: Boolean): DataResult> { + override suspend fun getFirstPageModuleItems( + canvasContext: CanvasContext, + moduleId: Long, + forceNetwork: Boolean + ): DataResult> { val moduleItems = moduleFacade.getModuleItems(moduleId) return DataResult.Success(moduleItems, apiType = ApiType.DB) } @@ -51,4 +63,18 @@ class ModuleListLocalDataSource(private val tabDao: TabDao, private val moduleFa override suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { return courseSettingsDao.findByCourseId(courseId)?.toApiModel() } + + override suspend fun getModuleItemCheckpoints(courseId: String, forceNetwork: Boolean): List { + val checkpointEntities = checkpointDao.findByCourseIdWithModuleItem(courseId.toLongOrNull().orDefault()) + + return checkpointEntities + .filter { it.moduleItemId != null } + .groupBy { it.moduleItemId } + .map { (moduleItemId, checkpoints) -> + ModuleItemWithCheckpoints( + moduleItemId = moduleItemId.toString(), + checkpoints = checkpoints.map { it.toModuleItemCheckpoint() } + ) + } + } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSource.kt index 76e6b84a65..f95e49841a 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSource.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSource.kt @@ -20,6 +20,8 @@ import com.instructure.canvasapi2.apis.CourseAPI import com.instructure.canvasapi2.apis.ModuleAPI import com.instructure.canvasapi2.apis.TabAPI import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.managers.graphql.ModuleItemWithCheckpoints +import com.instructure.canvasapi2.managers.graphql.ModuleManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.ModuleItem @@ -31,7 +33,8 @@ import com.instructure.canvasapi2.utils.depaginate class ModuleListNetworkDataSource( private val moduleApi: ModuleAPI.ModuleInterface, private val tabApi: TabAPI.TabsInterface, - private val courseApi: CourseAPI.CoursesInterface) : ModuleListDataSource { + private val courseApi: CourseAPI.CoursesInterface, + private val moduleManager: ModuleManager) : ModuleListDataSource { override suspend fun getAllModuleObjects(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) @@ -69,4 +72,8 @@ class ModuleListNetworkDataSource( val restParams = RestParams(isForceReadFromNetwork = forceNetwork) return courseApi.getCourseSettings(courseId, restParams).dataOrNull } + + override suspend fun getModuleItemCheckpoints(courseId: String, forceNetwork: Boolean): List { + return moduleManager.getModuleItemCheckpoints(courseId, forceNetwork) + } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt b/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt index 43404506f2..8ceddd27c8 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt @@ -344,7 +344,11 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { repository.markAsRead(canvasContext, moduleItem) // Update the module item locally, needed to unlock modules as the user ViewPages through them - getCurrentModuleItem(currentPos)?.completionRequirement?.completed = true + // Only mark as completed if the requirement is satisfied by viewing (not must_mark_done) + val completionRequirement = getCurrentModuleItem(currentPos)?.completionRequirement + if (completionRequirement?.type != ModuleItem.MUST_MARK_DONE) { + completionRequirement?.completed = true + } setupNextModule(getModuleItemGroup(currentPos)) diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt index 3f1f3aebe7..edbbc5abd9 100644 --- a/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt @@ -45,6 +45,8 @@ import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.navigation.WebViewRouter import com.instructure.pandautils.utils.BooleanArg +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.FileDownloader import com.instructure.pandautils.utils.NullableStringArg import com.instructure.pandautils.utils.ParcelableArg import com.instructure.pandautils.utils.ViewStyler @@ -79,6 +81,12 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { @Inject lateinit var webViewRouter: WebViewRouter + @Inject + lateinit var featureFlagProvider: FeatureFlagProvider + + @Inject + lateinit var fileDownloader: FileDownloader + private var loadHtmlJob: Job? = null private var pageName: String? by NullableStringArg(key = PAGE_NAME) private var page: Page by ParcelableArg(default = Page(), key = PAGE) @@ -226,13 +234,26 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { val body = """""" + page.body.orEmpty() // Load the html with the helper function to handle iframe cases - loadHtmlJob = canvasWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), body, { - canvasWebViewWrapper.loadHtml(it, page.title, baseUrl = page.htmlUrl) - }) { - RouteMatcher.route(requireActivity(), LtiLaunchFragment.makeSessionlessLtiUrlRoute(requireActivity(), canvasContext, it)) - } + loadHtmlJob = canvasWebViewWrapper.webView.loadHtmlWithIframes( + requireContext(), + featureFlagProvider, + body, + { + canvasWebViewWrapper.loadHtml(it, page.title, baseUrl = page.htmlUrl) + }, + courseId = canvasContext.id, + onLtiButtonPressed = { + RouteMatcher.route( + requireActivity(), + LtiLaunchFragment.makeSessionlessLtiUrlRoute( + requireActivity(), + canvasContext, + it + ) + ) + }) } else if (page.body == null || page.body?.endsWith("") == true) { - loadHtml(resources.getString(R.string.noPageFound), "text/html", "utf-8", null) + populateWebView(resources.getString(R.string.noPageFound), getString(R.string.pages)) } toolbar.title = title() @@ -283,9 +304,9 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { // We want it to be lowercase. context = context.lowercase(Locale.getDefault()) - loadHtml(resources.getString(R.string.noPagesInContext) + " " + context, "text/html", "utf-8", null) + populateWebView(resources.getString(R.string.noPagesInContext) + " " + context, getString(R.string.pages)) } else { - loadHtml(resources.getString(R.string.noPageFound), "text/html", "utf-8", null) + populateWebView(resources.getString(R.string.noPageFound), getString(R.string.pages)) } } @@ -351,6 +372,10 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { override fun handleBackPressed() = false + override fun downloadInternalMedia(mime: String?, url: String?, filename: String?) { + fileDownloader.downloadFileToDevice(url, filename, mime) + } + companion object { const val PAGE_NAME = "pageDetailsName" const val PAGE = "pageDetails" diff --git a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentBasicFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentBasicFragment.kt index 9591f8f08f..6fcb4b6f85 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentBasicFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentBasicFragment.kt @@ -32,6 +32,7 @@ import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.FeatureFlagProvider import com.instructure.pandautils.utils.OnBackStackChangedEvent import com.instructure.pandautils.utils.ParcelableArg import com.instructure.pandautils.utils.ViewStyler @@ -44,6 +45,7 @@ import com.instructure.pandautils.views.CanvasWebView import com.instructure.student.R import com.instructure.student.databinding.FragmentAssignmentBasicBinding import com.instructure.student.router.RouteMatcher +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Job import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -51,10 +53,15 @@ import org.greenrobot.eventbus.ThreadMode import java.util.Calendar import java.util.Date import java.util.Locale +import javax.inject.Inject +@AndroidEntryPoint @ScreenView(SCREEN_VIEW_ASSIGNMENT_BASIC) class AssignmentBasicFragment : ParentFragment() { + @Inject + lateinit var featureFlagProvider: FeatureFlagProvider + private val binding by viewBinding(FragmentAssignmentBasicBinding::bind) private var canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) @@ -150,11 +157,11 @@ class AssignmentBasicFragment : ParentFragment() { description = "

" + getString(R.string.noDescription) + "

" } - loadHtmlJob = assignmentWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), description, { + loadHtmlJob = assignmentWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), featureFlagProvider, description, { assignmentWebViewWrapper.loadHtml(it, assignment.name) }, { RouteMatcher.route(requireActivity(), LtiLaunchFragment.makeSessionlessLtiUrlRoute(requireActivity(), canvasContext, it)) - }) + }, courseId = canvasContext.id) } //endregion diff --git a/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt index 5cc0d9036f..fdbea2334b 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt @@ -57,7 +57,7 @@ class BasicQuizViewFragment : InternalWebviewFragment() { private var apiURL: String? by NullableStringArg() private var quiz: Quiz? by NullableParcelableArg() @get:PageViewUrlParam("quizId") - var quizId: Long by LongArg() + var quizId: Long by LongArg(key = RouterParams.QUIZ_ID) private var isTakingQuiz = false override fun title(): String = getString(R.string.quizzes) @@ -210,11 +210,29 @@ class BasicQuizViewFragment : InternalWebviewFragment() { private suspend fun processQuizDetails(url: String) { // Only show the lock if submissions are empty, otherwise let them view their submission - if (quiz?.lockInfo != null && awaitApi { QuizManager.getFirstPageQuizSubmissions(canvasContext, quiz!!.id, true, it) }.quizSubmissions.isEmpty()) { - populateWebView(LockInfoHTMLHelper.getLockedInfoHTML(quiz?.lockInfo!!, requireContext(), R.string.lockedQuizDesc)) + if (quiz?.lockInfo != null && awaitApi { + QuizManager.getFirstPageQuizSubmissions( + canvasContext, + quiz!!.id, + true, + it + ) + }.quizSubmissions.isEmpty()) { + populateWebView( + LockInfoHTMLHelper.getLockedInfoHTML( + quiz?.lockInfo!!, + requireContext(), + R.string.lockedQuizDesc + ) + ) } else { val authenticatedUrl = tryOrNull { - awaitApi { OAuthManager.getAuthenticatedSession(url, it) }.sessionUrl + awaitApi { + OAuthManager.getAuthenticatedSession( + url, it, + ApiPrefs.overrideDomains[canvasContext.id] + ) + }.sessionUrl } getCanvasWebView()?.loadUrl(authenticatedUrl ?: url, APIHelper.referrer) } @@ -276,6 +294,7 @@ class BasicQuizViewFragment : InternalWebviewFragment() { Bundle().apply { putString(Const.URL, url) putParcelable(Const.QUIZ, quiz) + putLong(RouterParams.QUIZ_ID, quiz.id) })) } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/BookmarksFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/BookmarksFragment.kt index ef4ed3995a..af2dbf27e3 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/BookmarksFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/BookmarksFragment.kt @@ -39,7 +39,12 @@ import com.instructure.interactions.router.Route import com.instructure.pandautils.analytics.SCREEN_VIEW_BOOKMARKS import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding -import com.instructure.pandautils.utils.* +import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.ViewStyler +import com.instructure.pandautils.utils.hideKeyboard +import com.instructure.pandautils.utils.isTablet +import com.instructure.pandautils.utils.setupAsBackButton +import com.instructure.pandautils.utils.setupAsCloseButton import com.instructure.student.R import com.instructure.student.activity.BookmarkShortcutActivity import com.instructure.student.adapter.BookmarkRecyclerAdapter @@ -129,7 +134,9 @@ class BookmarksFragment : ParentFragment() { recyclerAdapter = BookmarkRecyclerAdapter(requireContext(), isShortcutActivity, object : BookmarkAdapterToFragmentCallback { override fun onRowClicked(bookmark: Bookmark, position: Int, isOpenDetail: Boolean) { bookmarkSelectedCallback(bookmark) - dismiss() + if (isShortcutActivity) { + dismiss() + } } override fun onRefreshFinished() { diff --git a/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt index f09722105b..e64767651d 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt @@ -224,6 +224,10 @@ open class InternalWebviewFragment : ParentFragment() { } } + override fun downloadInternalMedia(mime: String?, url: String?, filename: String?) { + this@InternalWebviewFragment.downloadInternalMedia(mime, url, filename) + } + }) if (savedInstanceState != null) { @@ -231,6 +235,8 @@ open class InternalWebviewFragment : ParentFragment() { } } + open fun downloadInternalMedia(mime: String?, url: String?, filename: String?) = Unit + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) if (shouldLoadUrl) { diff --git a/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt index a4b3150195..a8d19ad08c 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt @@ -278,12 +278,15 @@ class NotificationListFragment : ParentFragment(), Bookmarkable, FragmentManager SUBMISSION -> { if (canvasContext !is Course) return - if (streamItem.assignment == null) { + val assignment = streamItem.assignment + + if (assignment == null) { RouteMatcher.route(activity, AssignmentDetailsFragment.makeRoute(canvasContext, streamItem.assignmentId)) } else { // Add an empty submission with the grade to the assignment so that we can see the score. - streamItem.assignment?.submission = Submission(grade = streamItem.grade) - RouteMatcher.route(activity, AssignmentDetailsFragment.makeRoute(canvasContext, streamItem.assignment!!.id)) + assignment.submission = Submission(grade = streamItem.grade) + val assignmentId = assignment.discussionTopicHeader?.assignmentId ?: assignment.id + RouteMatcher.route(activity, AssignmentDetailsFragment.makeRoute(canvasContext, assignmentId)) } null } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/StudioWebViewFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/StudioWebViewFragment.kt index 5d18d56b01..33af2a2537 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/StudioWebViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/StudioWebViewFragment.kt @@ -51,6 +51,7 @@ import javax.inject.Inject class StudioWebViewFragment : InternalWebviewFragment() { val assignmentId: Long by LongArg(key = Const.ASSIGNMENT_ID) val assignmentName: String by StringArg(key = Const.ASSIGNMENT_NAME) + val attempt: Long by LongArg(key = Const.SUBMISSION_ATTEMPT, default = 1L) @Inject lateinit var submissionHelper: SubmissionHelper @@ -146,7 +147,7 @@ class StudioWebViewFragment : InternalWebviewFragment() { url = StringEscapeUtils.unescapeJava(url) // Upload the url as a submission - submissionHelper.startStudioSubmission(canvasContext, assignmentId, assignmentName, url) + submissionHelper.startStudioSubmission(canvasContext, assignmentId, assignmentName, url, attempt) // Close this page navigation?.popCurrentFragment() @@ -167,7 +168,7 @@ class StudioWebViewFragment : InternalWebviewFragment() { } } else null - fun makeRoute(canvasContext: CanvasContext, url: String, title: String, authenticate: Boolean, assignment: Assignment): Route = + fun makeRoute(canvasContext: CanvasContext, url: String, title: String, authenticate: Boolean, assignment: Assignment, attempt: Long = 1L): Route = Route( StudioWebViewFragment::class.java, canvasContext, canvasContext.makeBundle().apply { @@ -176,6 +177,7 @@ class StudioWebViewFragment : InternalWebviewFragment() { putString(Const.ACTION_BAR_TITLE, title) putString(Const.ASSIGNMENT_NAME, assignment.name) putLong(Const.ASSIGNMENT_ID, assignment.id) + putLong(Const.SUBMISSION_ATTEMPT, attempt) }) fun validRoute(route: Route) : Boolean { diff --git a/apps/student/src/main/java/com/instructure/student/holders/NotificationViewHolder.kt b/apps/student/src/main/java/com/instructure/student/holders/NotificationViewHolder.kt index 71e039750a..9b1dd46bf3 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/NotificationViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/holders/NotificationViewHolder.kt @@ -28,6 +28,7 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.StreamItem import com.instructure.canvasapi2.utils.convertScoreToLetterGrade import com.instructure.pandautils.utils.ColorKeeper +import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.ThemePrefs import com.instructure.pandautils.utils.color import com.instructure.pandautils.utils.orDefault @@ -103,8 +104,29 @@ class NotificationViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) icon.contentDescription = context.getString(R.string.announcementIcon) } StreamItem.Type.SUBMISSION -> { - drawableResId = R.drawable.ic_assignment - icon.contentDescription = context.getString(R.string.assignmentIcon) + val subAssignmentTag = item.assignment?.subAssignmentTag + val isCheckpointSubmission = !subAssignmentTag.isNullOrEmpty() + + if (isCheckpointSubmission) { + drawableResId = R.drawable.ic_discussion + icon.contentDescription = context.getString(R.string.discussionIcon) + + checkpointLabel.text = when (subAssignmentTag) { + Const.REPLY_TO_TOPIC -> { + context.getString(R.string.reply_to_topic) + } + Const.REPLY_TO_ENTRY -> { + val count = item.assignment?.discussionTopicHeader?.replyRequiredCount ?: 0 + context.getString(R.string.additional_replies, count) + } + else -> "" + } + checkpointLabel.setVisible() + } else { + drawableResId = R.drawable.ic_assignment + icon.contentDescription = context.getString(R.string.assignmentIcon) + checkpointLabel.setGone() + } val course = item.canvasContext as? Course val restrictQuantitativeData = course?.settings?.restrictQuantitativeData.orDefault() diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/SubmissionUtils.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/SubmissionUtils.kt index c8c5f28f96..b8ed586feb 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/SubmissionUtils.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/SubmissionUtils.kt @@ -107,7 +107,8 @@ fun uploadAudioRecording(submissionHelper: SubmissionHelper, file: File, assignm assignmentId = assignment.id, assignmentGroupCategoryId = assignment.groupCategoryId, assignmentName = assignment.name, - mediaFilePath = file.path + mediaFilePath = file.path, + mediaSource = "audio_recorder" ) } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionUploadFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionUploadFragment.kt index d1d16c0c43..8f674daed0 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionUploadFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionUploadFragment.kt @@ -51,6 +51,7 @@ class AnnotationSubmissionUploadFragment : BaseCanvasFragment() { private var assignmentId by LongArg(key = Const.ASSIGNMENT_ID) private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) private var assignmentName by StringArg(key = Const.ASSIGNMENT_NAME) + private var attempt by LongArg(key = Const.SUBMISSION_ATTEMPT, default = 1L) private val viewModel: AnnotationSubmissionViewModel by viewModels() @@ -88,7 +89,7 @@ class AnnotationSubmissionUploadFragment : BaseCanvasFragment() { toolbar.setMenu(R.menu.menu_submit_generic) { when (it.itemId) { R.id.menuSubmit -> { - submissionHelper.startStudentAnnotationSubmission(canvasContext, assignmentId, assignmentName, annotatableAttachmentId) + submissionHelper.startStudentAnnotationSubmission(canvasContext, assignmentId, assignmentName, annotatableAttachmentId, attempt) requireActivity().onBackPressed() } } @@ -111,7 +112,8 @@ class AnnotationSubmissionUploadFragment : BaseCanvasFragment() { annotatableAttachmentId: Long, submissionId: Long, assignmentId: Long, - assignmentName: String + assignmentName: String, + attempt: Long = 1L ): Route { val bundle = Bundle().apply { putParcelable(Const.CANVAS_CONTEXT, canvasContext) @@ -119,6 +121,7 @@ class AnnotationSubmissionUploadFragment : BaseCanvasFragment() { putLong(SUBMISSION_ID, submissionId) putLong(Const.ASSIGNMENT_ID, assignmentId) putString(Const.ASSIGNMENT_NAME, assignmentName) + putLong(Const.SUBMISSION_ATTEMPT, attempt) } return Route(AnnotationSubmissionUploadFragment::class.java, canvasContext, bundle) } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionViewModel.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionViewModel.kt index 57cb35e018..9d09c607ff 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionViewModel.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionViewModel.kt @@ -59,6 +59,7 @@ class AnnotationSubmissionViewModel @Inject constructor( ViewState.Error(resources.getString(R.string.failedToLoadSubmission)) } } catch (e: Exception) { + e.printStackTrace() _state.value = ViewState.Error(resources.getString(R.string.failedToLoadSubmission)) } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadEffectHandler.kt index f99647fdac..d806afc883 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadEffectHandler.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadEffectHandler.kt @@ -18,7 +18,6 @@ package com.instructure.student.mobius.assignmentDetails.submission.picker import android.app.Activity import android.content.Context -import android.content.Intent import android.net.Uri import androidx.core.content.FileProvider import com.instructure.canvasapi2.models.postmodels.FileSubmitObject @@ -32,7 +31,6 @@ import com.instructure.pandautils.utils.getFragmentActivity import com.instructure.pandautils.utils.remove import com.instructure.pandautils.utils.requestPermissions import com.instructure.student.R -import com.instructure.student.features.documentscanning.DocumentScanningActivity import com.instructure.student.mobius.assignmentDetails.isIntentAvailable import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionMode.CommentAttachment import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionMode.FileSubmission @@ -117,9 +115,6 @@ class PickerSubmissionUploadEffectHandler( PickerSubmissionUploadEffect.LaunchSelectFile -> { launchSelectFile() } - PickerSubmissionUploadEffect.LaunchDocumentScanning -> { - launchDocumentScanning() - } is PickerSubmissionUploadEffect.LoadFileContents -> { loadFile(effect.allowedExtensions, effect.uri, context) } @@ -135,12 +130,21 @@ class PickerSubmissionUploadEffectHandler( private fun handleSubmit(model: PickerSubmissionUploadModel) { when (model.mode) { MediaSubmission -> { + val file = model.files.first() + val mediaType = when { + file.contentType?.startsWith("video/") == true -> "video" + file.contentType?.startsWith("audio/") == true -> "audio" + else -> null + } submissionHelper.startMediaSubmission( canvasContext = model.canvasContext, assignmentId = model.assignmentId, assignmentName = model.assignmentName, assignmentGroupCategoryId = model.assignmentGroupCategoryId, - mediaFilePath = model.files.first().fullPath + mediaFilePath = file.fullPath, + attempt = model.attemptId ?: 1L, + mediaType = mediaType, + mediaSource = model.mediaSource ) } FileSubmission -> { @@ -149,7 +153,8 @@ class PickerSubmissionUploadEffectHandler( assignmentId = model.assignmentId, assignmentName = model.assignmentName, assignmentGroupCategoryId = model.assignmentGroupCategoryId, - files = ArrayList(model.files) + files = ArrayList(model.files), + attempt = model.attemptId ?: 1L ) } CommentAttachment -> { @@ -210,17 +215,6 @@ class PickerSubmissionUploadEffectHandler( } } - private fun launchDocumentScanning() { - // Get camera permission if we need it - if (needsPermissions( - PickerSubmissionUploadEvent.DocumentScanningClicked, - PermissionUtils.CAMERA - ) - ) return - val intent = Intent(context, DocumentScanningActivity::class.java) - (context.getFragmentActivity()).startActivityForResult(intent, REQUEST_DOCUMENT_SCANNING) - } - private fun launchCamera() { // Get camera permission if we need it if (needsPermissions( diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadModels.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadModels.kt index 0a2ca09bfa..610aa66f74 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadModels.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadModels.kt @@ -25,7 +25,6 @@ sealed class PickerSubmissionUploadEvent { object CameraClicked : PickerSubmissionUploadEvent() object GalleryClicked : PickerSubmissionUploadEvent() object SelectFileClicked : PickerSubmissionUploadEvent() - object DocumentScanningClicked : PickerSubmissionUploadEvent() data class OnFileSelected(val uri: Uri) : PickerSubmissionUploadEvent() data class OnFileRemoved(val fileIndex: Int) : PickerSubmissionUploadEvent() data class OnFileAdded(val file: FileSubmitObject?) : PickerSubmissionUploadEvent() @@ -35,7 +34,6 @@ sealed class PickerSubmissionUploadEffect { object LaunchCamera : PickerSubmissionUploadEffect() object LaunchGallery : PickerSubmissionUploadEffect() object LaunchSelectFile : PickerSubmissionUploadEffect() - object LaunchDocumentScanning : PickerSubmissionUploadEffect() data class HandleSubmit(val model: PickerSubmissionUploadModel) : PickerSubmissionUploadEffect() data class LoadFileContents(val uri: Uri, val allowedExtensions: List) : PickerSubmissionUploadEffect() @@ -52,7 +50,8 @@ data class PickerSubmissionUploadModel( val mediaFileUri: Uri? = null, val files: List = emptyList(), val isLoadingFile: Boolean = false, - val attemptId: Long? = null + val attemptId: Long? = null, + val mediaSource: String? = null ) enum class PickerSubmissionMode { diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadUpdate.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadUpdate.kt index 00b68bcedb..5be56f5e1b 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadUpdate.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadUpdate.kt @@ -26,7 +26,8 @@ class PickerSubmissionUploadUpdate : return if (model.mediaFileUri == null) { First.first(model) } else { - First.first(model.copy(isLoadingFile = true), setOf(PickerSubmissionUploadEffect.LoadFileContents(model.mediaFileUri, model.allowedExtensions))) + val source = model.mediaSource ?: "camera" + First.first(model.copy(isLoadingFile = true, mediaSource = source), setOf(PickerSubmissionUploadEffect.LoadFileContents(model.mediaFileUri, model.allowedExtensions))) } } @@ -35,10 +36,9 @@ class PickerSubmissionUploadUpdate : event: PickerSubmissionUploadEvent ): Next = when (event) { PickerSubmissionUploadEvent.SubmitClicked -> Next.dispatch(setOf(PickerSubmissionUploadEffect.HandleSubmit(model))) - PickerSubmissionUploadEvent.CameraClicked -> Next.dispatch(setOf(PickerSubmissionUploadEffect.LaunchCamera)) - PickerSubmissionUploadEvent.GalleryClicked -> Next.dispatch(setOf(PickerSubmissionUploadEffect.LaunchGallery)) - PickerSubmissionUploadEvent.SelectFileClicked -> Next.dispatch(setOf(PickerSubmissionUploadEffect.LaunchSelectFile)) - PickerSubmissionUploadEvent.DocumentScanningClicked -> Next.dispatch(setOf(PickerSubmissionUploadEffect.LaunchDocumentScanning)) + PickerSubmissionUploadEvent.CameraClicked -> Next.next(model.copy(mediaSource = "camera"), setOf(PickerSubmissionUploadEffect.LaunchCamera)) + PickerSubmissionUploadEvent.GalleryClicked -> Next.next(model.copy(mediaSource = "library"), setOf(PickerSubmissionUploadEffect.LaunchGallery)) + PickerSubmissionUploadEvent.SelectFileClicked -> Next.next(model.copy(mediaSource = "files"), setOf(PickerSubmissionUploadEffect.LaunchSelectFile)) is PickerSubmissionUploadEvent.OnFileSelected -> { Next.next( model.copy(isLoadingFile = true), diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/BasePickerSubmissionUploadFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/BasePickerSubmissionUploadFragment.kt index 48c6196a96..7d29f79651 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/BasePickerSubmissionUploadFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/BasePickerSubmissionUploadFragment.kt @@ -35,7 +35,9 @@ import com.instructure.student.mobius.assignmentDetails.submission.picker.Picker import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadModel import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadPresenter import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadUpdate +import com.instructure.pandautils.utils.NullableStringArg import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerSubmissionUploadFragment.Companion.INVALID_ATTEMPT +import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerSubmissionUploadFragment.Companion.MEDIA_SOURCE import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerSubmissionUploadFragment.Companion.PICKER_MODE import com.instructure.student.mobius.common.ui.MobiusFragment @@ -48,6 +50,7 @@ abstract class BasePickerSubmissionUploadFragment : private val mode by SerializableArg(key = PICKER_MODE, default = PickerSubmissionMode.FileSubmission) private val mediaUri by NullableParcelableArg(key = Const.PASSED_URI, default = null) private var attemptId by LongArg(key = Const.SUBMISSION_ATTEMPT) + private val initialMediaSource by NullableStringArg(key = MEDIA_SOURCE) override fun makeUpdate() = PickerSubmissionUploadUpdate() @@ -64,6 +67,7 @@ abstract class BasePickerSubmissionUploadFragment : if (mode.isForComment || mode.isMediaSubmission) emptyList() else assignment.allowedExtensions, mode, mediaUri, - attemptId = attemptId.takeIf { it != INVALID_ATTEMPT } + attemptId = attemptId.takeIf { it != INVALID_ATTEMPT }, + mediaSource = initialMediaSource ) } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadFragment.kt index 710522708b..bf9a72866f 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadFragment.kt @@ -42,6 +42,7 @@ class PickerSubmissionUploadFragment : BasePickerSubmissionUploadFragment() { const val PICKER_MODE = "pickerMode" const val INVALID_ATTEMPT = -1L + const val MEDIA_SOURCE = "mediaSource" private fun validRoute(route: Route) = route.canvasContext?.isCourseOrGroup == true && route.arguments.containsKey(Const.ASSIGNMENT) @@ -66,12 +67,16 @@ class PickerSubmissionUploadFragment : BasePickerSubmissionUploadFragment() { fun makeRoute( canvasContext: CanvasContext, assignment: Assignment, - mediaUri: Uri + mediaUri: Uri, + attemptId: Long = 1L, + mediaSource: String? = null ): Route { val bundle = canvasContext.makeBundle { putParcelable(Const.ASSIGNMENT, assignment) putParcelable(Const.PASSED_URI, mediaUri) putSerializable(PICKER_MODE, PickerSubmissionMode.MediaSubmission) + putLong(Const.SUBMISSION_ATTEMPT, attemptId) + mediaSource?.let { putString(MEDIA_SOURCE, it) } } return Route(PickerSubmissionUploadFragment::class.java, canvasContext, bundle) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadView.kt index 7a6d08e6ce..8a5f4cfe74 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadView.kt @@ -60,7 +60,6 @@ class PickerSubmissionUploadView(inflater: LayoutInflater, parent: ViewGroup, va binding.sourceDevice.setOnClickListener { consumer?.accept(PickerSubmissionUploadEvent.SelectFileClicked) } binding.sourceGallery.setOnClickListener { consumer?.accept(PickerSubmissionUploadEvent.GalleryClicked) } - binding.sourceDocumentScanning.setOnClickListener { consumer?.accept(PickerSubmissionUploadEvent.DocumentScanningClicked) } } override fun onConnect(output: Consumer) { diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadEffectHandler.kt index 70d7d68f6f..721c061a48 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadEffectHandler.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadEffectHandler.kt @@ -28,7 +28,7 @@ class TextSubmissionUploadEffectHandler(private val submissionHelper: Submission override fun accept(effect: TextSubmissionUploadEffect) { when (effect) { is TextSubmissionUploadEffect.SubmitText -> { - submissionHelper.startTextSubmission(effect.canvasContext, effect.assignmentId, effect.assignmentName, effect.text) + submissionHelper.startTextSubmission(effect.canvasContext, effect.assignmentId, effect.assignmentName, effect.text, effect.attempt) view?.goBack() } is TextSubmissionUploadEffect.InitializeText -> view?.setInitialSubmissionText(effect.text) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadModels.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadModels.kt index c58739ffd0..50abd26911 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadModels.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadModels.kt @@ -30,7 +30,7 @@ sealed class TextSubmissionUploadEvent { } sealed class TextSubmissionUploadEffect { - data class SubmitText(val text: String, val canvasContext: CanvasContext, val assignmentId: Long, val assignmentName: String?) : TextSubmissionUploadEffect() + data class SubmitText(val text: String, val canvasContext: CanvasContext, val assignmentId: Long, val assignmentName: String?, val attempt: Long) : TextSubmissionUploadEffect() data class InitializeText(val text: String) : TextSubmissionUploadEffect() data class AddImage(val uri: Uri, val canvasContext: CanvasContext) : TextSubmissionUploadEffect() data class SaveDraft(val text: String, val canvasContext: CanvasContext, val assignmentId: Long, val assignmentName: String?) : TextSubmissionUploadEffect() @@ -44,5 +44,6 @@ data class TextSubmissionUploadModel( val assignmentName: String?, val initialText: String? = null, val isFailure: Boolean = false, - val isSubmittable: Boolean = false + val isSubmittable: Boolean = false, + val attempt: Long = 1L ) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadUpdate.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadUpdate.kt index 6b9fd0fd4a..24048da996 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadUpdate.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadUpdate.kt @@ -41,7 +41,8 @@ class TextSubmissionUploadUpdate : event.text, model.canvasContext, model.assignmentId, - model.assignmentName + model.assignmentName, + model.attempt ) ) ) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/ui/BaseTextSubmissionUploadFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/ui/BaseTextSubmissionUploadFragment.kt index bb69b60e51..1ab839ee17 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/ui/BaseTextSubmissionUploadFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/ui/BaseTextSubmissionUploadFragment.kt @@ -42,6 +42,7 @@ abstract class BaseTextSubmissionUploadFragment : MobiusFragment { - submissionHelper.startUrlSubmission(effect.course, effect.assignmentId, effect.assignmentName, effect.url) + submissionHelper.startUrlSubmission(effect.course, effect.assignmentId, effect.assignmentName, effect.url, effect.attempt) view?.goBack() } is UrlSubmissionUploadEffect.InitializeUrl -> { diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/UrlSubmissionUploadModels.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/UrlSubmissionUploadModels.kt index 5837859711..6fad03e19a 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/UrlSubmissionUploadModels.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/UrlSubmissionUploadModels.kt @@ -26,7 +26,7 @@ sealed class UrlSubmissionUploadEvent { sealed class UrlSubmissionUploadEffect { data class ShowUrlPreview(val url: String) : UrlSubmissionUploadEffect() - data class SubmitUrl(val url: String, val course: CanvasContext, val assignmentId: Long, val assignmentName: String?) : UrlSubmissionUploadEffect() + data class SubmitUrl(val url: String, val course: CanvasContext, val assignmentId: Long, val assignmentName: String?, val attempt: Long) : UrlSubmissionUploadEffect() data class InitializeUrl(val url: String?) : UrlSubmissionUploadEffect() } @@ -41,5 +41,6 @@ data class UrlSubmissionUploadModel( val initialUrl: String? = null, val isFailure: Boolean = false, val urlError: MalformedUrlError = MalformedUrlError.NONE, - val isSubmittable: Boolean = false + val isSubmittable: Boolean = false, + val attempt: Long = 1L ) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/UrlSubmissionUploadUpdate.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/UrlSubmissionUploadUpdate.kt index 11cde1fc86..d06dff87a0 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/UrlSubmissionUploadUpdate.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/UrlSubmissionUploadUpdate.kt @@ -51,7 +51,7 @@ class UrlSubmissionUploadUpdate : UpdateInit { - Next.dispatch(Effects.effects((UrlSubmissionUploadEffect.SubmitUrl(event.url, model.course, model.assignmentId, model.assignmentName)) as UrlSubmissionUploadEffect)) + Next.dispatch(Effects.effects((UrlSubmissionUploadEffect.SubmitUrl(event.url, model.course, model.assignmentId, model.assignmentName, model.attempt)) as UrlSubmissionUploadEffect)) } } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/ui/BaseUrlSubmissionUploadFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/ui/BaseUrlSubmissionUploadFragment.kt index 6b2e4856f8..a019a5799a 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/ui/BaseUrlSubmissionUploadFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/ui/BaseUrlSubmissionUploadFragment.kt @@ -43,6 +43,7 @@ abstract class BaseUrlSubmissionUploadFragment : MobiusFragment = UrlSubmissionUploadUpdate() @@ -50,5 +51,5 @@ abstract class BaseUrlSubmissionUploadFragment : MobiusFragment = UrlSubmissionUploadPresenter - override fun makeInitModel(): UrlSubmissionUploadModel = UrlSubmissionUploadModel(course, assignmentId, assignmentName, initialUrl, isFailure) + override fun makeInitModel(): UrlSubmissionUploadModel = UrlSubmissionUploadModel(course, assignmentId, assignmentName, initialUrl, isFailure, attempt = attempt) } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/ui/UrlSubmissionUploadFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/ui/UrlSubmissionUploadFragment.kt index 30fdc7e969..478a8d485e 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/ui/UrlSubmissionUploadFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/url/ui/UrlSubmissionUploadFragment.kt @@ -38,12 +38,13 @@ class UrlSubmissionUploadFragment : BaseUrlSubmissionUploadFragment() { companion object { - fun makeRoute(course: CanvasContext, assignmentId: Long, assignmentName: String? = "", initialUrl: String?, isFailure: Boolean = false): Route { + fun makeRoute(course: CanvasContext, assignmentId: Long, assignmentName: String? = "", initialUrl: String?, isFailure: Boolean = false, attempt: Long = 1L): Route { val bundle = course.makeBundle{ putLong(Const.ASSIGNMENT_ID, assignmentId) putString(Const.ASSIGNMENT_NAME, assignmentName) putString(Const.URL, initialUrl) putBoolean(Const.IS_FAILURE, isFailure) + putLong(Const.SUBMISSION_ATTEMPT, attempt) } return Route(null, UrlSubmissionUploadFragment::class.java, course, bundle) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/MediaSubmissionViewFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/MediaSubmissionViewFragment.kt index f49e8fb39c..81201f736a 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/MediaSubmissionViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/MediaSubmissionViewFragment.kt @@ -116,7 +116,7 @@ class MediaSubmissionViewFragment : BaseCanvasFragment() { private fun fetchMediaUri() { lifecycleScope.launch { - mediaUri = RouteUtils.getRedirectUrl(uri) + mediaUri = RouteUtils.getMediaUri(uri) if (isResumed) { attachMediaPlayer() } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt index fa6f602542..e0f3330e21 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt @@ -19,6 +19,7 @@ import android.app.Dialog import android.content.Intent import android.content.res.ColorStateList import android.net.Uri +import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -31,6 +32,7 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.LTITool import com.instructure.canvasapi2.models.Quiz import com.instructure.canvasapi2.utils.AnalyticsEventConstants +import com.instructure.canvasapi2.utils.AnalyticsParamConstants import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment import com.instructure.pandautils.features.lti.LtiLaunchFragment @@ -98,23 +100,42 @@ class SubmissionDetailsEmptyContentView( val dialog = builder.setView(binding.root).create() dialog.setOnShowListener { + val nextAttempt = (assignment.submission?.attempt ?: 0) + 1 setupDialogRow(dialog, binding.submissionEntryText, visibilities.textEntry) { + logEvent(AnalyticsEventConstants.SUBMIT_TEXTENTRY_SELECTED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString()) + }) showOnlineTextEntryView(assignment.id, assignment.name) } setupDialogRow(dialog, binding.submissionEntryWebsite, visibilities.urlEntry) { + logEvent(AnalyticsEventConstants.SUBMIT_URL_SELECTED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString()) + }) showOnlineUrlEntryView(assignment.id, assignment.name, canvasContext) } setupDialogRow(dialog, binding.submissionEntryFile, visibilities.fileUpload) { + logEvent(AnalyticsEventConstants.SUBMIT_FILEUPLOAD_SELECTED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString()) + }) showFileUploadView(assignment) } setupDialogRow(dialog, binding.submissionEntryMedia, visibilities.mediaRecording) { + logEvent(AnalyticsEventConstants.SUBMIT_MEDIARECORDING_SELECTED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString()) + }) showMediaRecordingView() } setupDialogRow(dialog, binding.submissionEntryStudio, visibilities.studioUpload) { + logEvent(AnalyticsEventConstants.SUBMIT_STUDIO_SELECTED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString()) + }) // The LTI info shouldn't be null if we are showing the Studio upload option showStudioUploadView(assignment, ltiToolUrl!!, ltiToolName!!) } setupDialogRow(dialog, binding.submissionEntryStudentAnnotation, visibilities.studentAnnotation) { + logEvent(AnalyticsEventConstants.SUBMIT_ANNOTATION_SELECTED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString()) + }) showStudentAnnotationView(assignment) } } @@ -130,12 +151,10 @@ class SubmissionDetailsEmptyContentView( } fun showOnlineTextEntryView(assignmentId: Long, assignmentName: String?, submittedText: String? = null, isFailure: Boolean = false) { - logEventWithOrigin(AnalyticsEventConstants.SUBMIT_TEXTENTRY_SELECTED) RouteMatcher.route(activity as FragmentActivity, TextSubmissionUploadFragment.makeRoute(canvasContext, assignmentId, assignmentName, submittedText, isFailure)) } fun showOnlineUrlEntryView(assignmentId: Long, assignmentName: String?, canvasContext: CanvasContext, submittedUrl: String? = null) { - logEventWithOrigin(AnalyticsEventConstants.SUBMIT_ONLINEURL_SELECTED) RouteMatcher.route(activity as FragmentActivity, UrlSubmissionUploadFragment.makeRoute(canvasContext, assignmentId, assignmentName, submittedUrl)) } @@ -161,7 +180,6 @@ class SubmissionDetailsEmptyContentView( } fun showMediaRecordingView() { - logEventWithOrigin(AnalyticsEventConstants.SUBMIT_MEDIARECORDING_SELECTED) val builder = AlertDialog.Builder(context) val binding = DialogSubmissionPickerMediaBinding.inflate(LayoutInflater.from(context), null, false) val dialog = builder.setView(binding.root).create() @@ -182,7 +200,6 @@ class SubmissionDetailsEmptyContentView( } private fun showStudioUploadView(assignment: Assignment, ltiUrl: String, studioLtiToolName: String) { - logEventWithOrigin(AnalyticsEventConstants.SUBMIT_STUDIO_SELECTED) RouteMatcher.route(activity as FragmentActivity, StudioWebViewFragment.makeRoute(canvasContext, ltiUrl, studioLtiToolName, true, assignment)) } @@ -214,7 +231,6 @@ class SubmissionDetailsEmptyContentView( } fun showFileUploadView(assignment: Assignment) { - logEventWithOrigin(AnalyticsEventConstants.SUBMIT_FILEUPLOAD_SELECTED) RouteMatcher.route(activity as FragmentActivity, PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, PickerSubmissionMode.FileSubmission)) } @@ -235,8 +251,6 @@ class SubmissionDetailsEmptyContentView( } fun showStudentAnnotationView(assignment: Assignment) { - logEvent(AnalyticsEventConstants.SUBMIT_STUDENT_ANNOTATION_SELECTED) - val submissionId = assignment.submission?.id if (submissionId != null) { RouteMatcher.route( diff --git a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionHelper.kt b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionHelper.kt index 7a45c793a3..8b4bd3cc1f 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionHelper.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionHelper.kt @@ -45,6 +45,7 @@ class SubmissionHelper( val submissionWork = OneTimeWorkRequest.Builder(SubmissionWorker::class.java) .setInputData(data.build()) .setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) + .addTag("SubmissionWorker") .build() workManager.enqueue(submissionWork) } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionWorker.kt b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionWorker.kt index 63d60c9854..9d2da52b50 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionWorker.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionWorker.kt @@ -24,6 +24,7 @@ import android.content.Context import android.content.Intent import android.content.pm.ServiceInfo import android.os.Build +import android.os.Bundle import android.util.Log import androidx.core.app.NotificationCompat import androidx.hilt.work.HiltWorker @@ -46,6 +47,7 @@ import com.instructure.canvasapi2.models.notorious.NotoriousResult import com.instructure.canvasapi2.models.postmodels.FileSubmitObject import com.instructure.canvasapi2.utils.Analytics import com.instructure.canvasapi2.utils.AnalyticsEventConstants +import com.instructure.canvasapi2.utils.AnalyticsParamConstants import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.FileUtils @@ -231,6 +233,12 @@ class SubmissionWorker @AssistedInject constructor( createFileSubmissionDao.deleteFilesForSubmissionId(submission.id) createSubmissionDao.deleteSubmissionById(submission.id) + analytics.logEvent(AnalyticsEventConstants.SUBMIT_MEDIARECORDING_SUCCEEDED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, submission.attempt.toString()) + submission.mediaType?.let { putString(AnalyticsParamConstants.MEDIA_TYPE, it) } + submission.mediaSource?.let { putString(AnalyticsParamConstants.MEDIA_SOURCE, it) } + }) + showCompleteNotification( context, submission, @@ -240,10 +248,20 @@ class SubmissionWorker @AssistedInject constructor( } ?: run { createSubmissionDao.setSubmissionError(true, submission.id) showErrorNotification(context, submission) + analytics.logEvent(AnalyticsEventConstants.SUBMIT_MEDIARECORDING_FAILED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, submission.attempt.toString()) + submission.mediaType?.let { putString(AnalyticsParamConstants.MEDIA_TYPE, it) } + submission.mediaSource?.let { putString(AnalyticsParamConstants.MEDIA_SOURCE, it) } + }) Result.failure() } }.onFailure { handleFileError(submission, 0, listOf(mediaFile), it?.message) + analytics.logEvent(AnalyticsEventConstants.SUBMIT_MEDIARECORDING_FAILED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, submission.attempt.toString()) + submission.mediaType?.let { putString(AnalyticsParamConstants.MEDIA_TYPE, it) } + submission.mediaSource?.let { putString(AnalyticsParamConstants.MEDIA_SOURCE, it) } + }) return Result.failure() } return Result.failure() @@ -337,7 +355,9 @@ class SubmissionWorker @AssistedInject constructor( return true } }).onSuccess { attachment -> - analytics.logEvent(AnalyticsEventConstants.SUBMIT_FILEUPLOAD_SUCCEEDED) + analytics.logEvent(AnalyticsEventConstants.SUBMIT_FILEUPLOAD_SUCCEEDED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, submission.attempt.toString()) + }) updateFileProgress( submission.id, 1.0f, @@ -357,7 +377,9 @@ class SubmissionWorker @AssistedInject constructor( attachmentIds.add(attachment.id) }.onFailure { - analytics.logEvent(AnalyticsEventConstants.SUBMIT_FILEUPLOAD_FAILED) + analytics.logEvent(AnalyticsEventConstants.SUBMIT_FILEUPLOAD_FAILED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, submission.attempt.toString()) + }) runBlocking { handleFileError(submission, index, attachments, it?.message) } @@ -610,10 +632,58 @@ class SubmissionWorker @AssistedInject constructor( return result.dataOrNull?.let { createSubmissionDao.deleteSubmissionById(submission.id) if (!result.dataOrThrow.late) showConfetti() + + when (submission.submissionType) { + Assignment.SubmissionType.ONLINE_TEXT_ENTRY.apiString -> { + analytics.logEvent(AnalyticsEventConstants.SUBMIT_TEXTENTRY_SUCCEEDED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, submission.attempt.toString()) + }) + } + Assignment.SubmissionType.ONLINE_URL.apiString -> { + analytics.logEvent(AnalyticsEventConstants.SUBMIT_URL_SUCCEEDED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, submission.attempt.toString()) + }) + } + Assignment.SubmissionType.BASIC_LTI_LAUNCH.apiString -> { + analytics.logEvent(AnalyticsEventConstants.SUBMIT_STUDIO_SUCCEEDED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, submission.attempt.toString()) + }) + } + Assignment.SubmissionType.STUDENT_ANNOTATION.apiString -> { + analytics.logEvent(AnalyticsEventConstants.SUBMIT_ANNOTATION_SUCCEEDED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, submission.attempt.toString()) + }) + } + } + Result.success() } ?: run { createSubmissionDao.setSubmissionError(true, submission.id) showErrorNotification(context, submission) + + when (submission.submissionType) { + Assignment.SubmissionType.ONLINE_TEXT_ENTRY.apiString -> { + analytics.logEvent(AnalyticsEventConstants.SUBMIT_TEXTENTRY_FAILED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, submission.attempt.toString()) + }) + } + Assignment.SubmissionType.ONLINE_URL.apiString -> { + analytics.logEvent(AnalyticsEventConstants.SUBMIT_URL_FAILED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, submission.attempt.toString()) + }) + } + Assignment.SubmissionType.BASIC_LTI_LAUNCH.apiString -> { + analytics.logEvent(AnalyticsEventConstants.SUBMIT_STUDIO_FAILED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, submission.attempt.toString()) + }) + } + Assignment.SubmissionType.STUDENT_ANNOTATION.apiString -> { + analytics.logEvent(AnalyticsEventConstants.SUBMIT_ANNOTATION_FAILED, Bundle().apply { + putString(AnalyticsParamConstants.ATTEMPT, submission.attempt.toString()) + }) + } + } + Result.failure() } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusEffectHandler.kt index 837b9dd728..13b005eb91 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusEffectHandler.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusEffectHandler.kt @@ -17,8 +17,6 @@ package com.instructure.student.mobius.syllabus import com.instructure.canvasapi2.apis.CalendarEventAPI -import com.instructure.canvasapi2.managers.CalendarEventManager -import com.instructure.canvasapi2.managers.CourseManager import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.exhaustive @@ -30,7 +28,7 @@ class SyllabusEffectHandler(private val repository: SyllabusRepository) : Effect override fun accept(effect: SyllabusEffect) { when (effect) { is SyllabusEffect.LoadData -> loadData(effect) - is SyllabusEffect.ShowAssignmentView -> view?.showAssignmentView(effect.assignment, effect.course) + is SyllabusEffect.ShowAssignmentView -> view?.showAssignmentView(effect.assignmentId, effect.course) is SyllabusEffect.ShowScheduleItemView -> view?.showScheduleItemView(effect.scheduleItem, effect.course) }.exhaustive } @@ -55,15 +53,29 @@ class SyllabusEffectHandler(private val repository: SyllabusRepository) : Effect val contextCodes = listOf(course.dataOrThrow.contextId) val assignments = repository.getCalendarEvents(true, CalendarEventAPI.CalendarEventType.ASSIGNMENT, null, null, contextCodes, effect.forceNetwork) + val subAssignments = repository.getCalendarEvents(true, CalendarEventAPI.CalendarEventType.SUB_ASSIGNMENT, null, null, contextCodes, effect.forceNetwork) val events = repository.getCalendarEvents(true, CalendarEventAPI.CalendarEventType.CALENDAR, null, null, contextCodes, effect.forceNetwork) + val plannerItems = repository.getPlannerItems(null, null, contextCodes, "all_ungraded_todo_items", effect.forceNetwork) + val endList = mutableListOf() assignments.map { endList.addAll(it) } + subAssignments.map { endList.addAll(it) } events.map { endList.addAll(it) } + plannerItems.map { items -> + // Filter out assignments, quizzes, and calendar events as they're already fetched above + val filteredItems = items.filter { + it.plannableType != com.instructure.canvasapi2.models.PlannableType.ASSIGNMENT && + it.plannableType != com.instructure.canvasapi2.models.PlannableType.SUB_ASSIGNMENT && + it.plannableType != com.instructure.canvasapi2.models.PlannableType.QUIZ && + it.plannableType != com.instructure.canvasapi2.models.PlannableType.CALENDAR_EVENT + } + endList.addAll(filteredItems.map { it.toScheduleItem() }) + } endList.sort() - summaryResult = if (assignments.isFail && events.isFail) { + summaryResult = if (assignments.isFail && subAssignments.isFail && events.isFail && plannerItems.isFail) { DataResult.Fail((assignments as? DataResult.Fail)?.failure) } else { DataResult.Success(endList) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusModels.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusModels.kt index 5bbda796bf..cfa2a9d075 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusModels.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusModels.kt @@ -29,7 +29,7 @@ sealed class SyllabusEvent { sealed class SyllabusEffect { data class LoadData(val courseId: Long, val forceNetwork: Boolean) : SyllabusEffect() - data class ShowAssignmentView(val assignment: Assignment, val course: Course) : SyllabusEffect() + data class ShowAssignmentView(val assignmentId: Long, val course: Course) : SyllabusEffect() data class ShowScheduleItemView(val scheduleItem: ScheduleItem, val course: Course) : SyllabusEffect() } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusPresenter.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusPresenter.kt index 99c20cf81c..87d14b8d64 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusPresenter.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusPresenter.kt @@ -80,19 +80,22 @@ object SyllabusPresenter : Presenter { } private fun getIcon(event: ScheduleItem): Int { - if (event.assignment?.isLocked == true || event.assignment?.lockExplanation?.takeIf { - it.isValid() && event.assignment?.lockDate?.before(Date()) == true + val assignment = event.assignment ?: event.subAssignment + if (assignment?.isLocked == true || assignment?.lockExplanation?.takeIf { + it.isValid() && assignment.lockDate?.before(Date()) == true } != null) { return com.instructure.student.R.drawable.ic_lock_lined } - return event.assignment?.let { + return assignment?.let { getAssignmentIcon(it) } ?: com.instructure.student.R.drawable.ic_calendar } private fun getAssignmentIcon(assignment: Assignment) = when { assignment.getSubmissionTypes().contains(Assignment.SubmissionType.ONLINE_QUIZ) -> com.instructure.student.R.drawable.ic_quiz + assignment.getSubmissionTypes().contains(Assignment.SubmissionType.EXTERNAL_TOOL) && + assignment.externalToolAttributes?.url?.contains("quiz-lti") == true -> com.instructure.student.R.drawable.ic_quiz assignment.getSubmissionTypes().contains(Assignment.SubmissionType.DISCUSSION_TOPIC) -> com.instructure.student.R.drawable.ic_discussion else -> com.instructure.student.R.drawable.ic_assignment } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusRepository.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusRepository.kt index 0b65dfd7ad..ab64229298 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusRepository.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusRepository.kt @@ -21,6 +21,7 @@ package com.instructure.student.mobius.syllabus import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.PlannerItem import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.pandautils.repository.Repository @@ -55,4 +56,14 @@ class SyllabusRepository( ): DataResult> { return dataSource().getCalendarEvents(allEvents, type, startDate, endDate, canvasContexts, forceNetwork) } + + suspend fun getPlannerItems( + startDate: String?, + endDate: String?, + contextCodes: List, + filter: String?, + forceNetwork: Boolean + ): DataResult> { + return dataSource().getPlannerItems(startDate, endDate, contextCodes, filter, forceNetwork) + } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusUpdate.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusUpdate.kt index 19c9727f29..a517d314f3 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusUpdate.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusUpdate.kt @@ -39,9 +39,13 @@ class SyllabusUpdate : UpdateInit( } is SyllabusEvent.SyllabusItemClicked -> { val item = model.events!!.dataOrThrow.find { it.itemId == event.itemId }!! + val assignment = item.assignment ?: item.subAssignment Next.dispatch(setOf( when { - item.assignment != null -> SyllabusEffect.ShowAssignmentView(item.assignment!!, model.course!!.dataOrThrow) + assignment != null -> { + val assignmentId = assignment.discussionTopicHeader?.assignmentId?.takeIf { it != 0L } ?: assignment.id + SyllabusEffect.ShowAssignmentView(assignmentId, model.course!!.dataOrThrow) + } else -> SyllabusEffect.ShowScheduleItemView(item, model.course!!.dataOrThrow) } )) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusDataSource.kt index 947c812ca9..6a02a472bd 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusDataSource.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusDataSource.kt @@ -21,6 +21,7 @@ package com.instructure.student.mobius.syllabus.datasource import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.PlannerItem import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult @@ -38,4 +39,12 @@ interface SyllabusDataSource { canvasContexts: List, forceNetwork: Boolean ): DataResult> + + suspend fun getPlannerItems( + startDate: String?, + endDate: String?, + contextCodes: List, + filter: String?, + forceNetwork: Boolean + ): DataResult> } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusLocalDataSource.kt index ec16f6e37b..ddd9e0a56d 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusLocalDataSource.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusLocalDataSource.kt @@ -21,16 +21,19 @@ package com.instructure.student.mobius.syllabus.datasource import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.PlannerItem import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.daos.PlannerItemDao import com.instructure.pandautils.room.offline.facade.CourseFacade import com.instructure.pandautils.room.offline.facade.ScheduleItemFacade class SyllabusLocalDataSource( private val courseSettingsDao: CourseSettingsDao, private val courseFacade: CourseFacade, - private val scheduleItemFacade: ScheduleItemFacade + private val scheduleItemFacade: ScheduleItemFacade, + private val plannerItemDao: PlannerItemDao ) : SyllabusDataSource { override suspend fun getCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { @@ -58,4 +61,31 @@ class SyllabusLocalDataSource( } } + + override suspend fun getPlannerItems( + startDate: String?, + endDate: String?, + contextCodes: List, + filter: String?, + forceNetwork: Boolean + ): DataResult> { + return try { + val courseIds = contextCodes.mapNotNull { contextCode -> + val parts = contextCode.split("_") + if (parts.size == 2 && parts[0] == "course") { + parts[1].toLongOrNull() + } else null + } + + val plannerItems = if (courseIds.isNotEmpty()) { + plannerItemDao.findByCourseIds(courseIds).map { it.toApiModel() } + } else { + emptyList() + } + + DataResult.Success(plannerItems) + } catch (e: Exception) { + DataResult.Fail() + } + } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusNetworkDataSource.kt index a237849f11..7eb7c7ded7 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusNetworkDataSource.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusNetworkDataSource.kt @@ -20,16 +20,19 @@ package com.instructure.student.mobius.syllabus.datasource import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.PlannerAPI import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.PlannerItem import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.depaginate class SyllabusNetworkDataSource( private val courseApi: CourseAPI.CoursesInterface, - private val calendarEventApi: CalendarEventAPI.CalendarEventInterface + private val calendarEventApi: CalendarEventAPI.CalendarEventInterface, + private val plannerApi: PlannerAPI.PlannerInterface ) : SyllabusDataSource { override suspend fun getCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { @@ -60,4 +63,21 @@ class SyllabusNetworkDataSource( restParams ).depaginate { calendarEventApi.next(it, restParams) } } + + override suspend fun getPlannerItems( + startDate: String?, + endDate: String?, + contextCodes: List, + filter: String?, + forceNetwork: Boolean + ): DataResult> { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return plannerApi.getPlannerItems( + startDate, + endDate, + contextCodes, + filter, + restParams + ).depaginate { plannerApi.nextPagePlannerItems(it, restParams) } + } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusView.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusView.kt index c2e363b71f..ed2214372b 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusView.kt @@ -21,9 +21,9 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.fragment.app.FragmentActivity import com.google.android.material.tabs.TabLayout -import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment import com.instructure.pandautils.features.calendarevent.details.EventFragment import com.instructure.pandautils.navigation.WebViewRouter import com.instructure.pandautils.utils.ViewStyler @@ -38,7 +38,6 @@ import com.instructure.student.R import com.instructure.student.databinding.FragmentSyllabusBinding import com.instructure.student.databinding.FragmentSyllabusEventsBinding import com.instructure.student.databinding.FragmentSyllabusWebviewBinding -import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment import com.instructure.student.mobius.common.ui.MobiusView import com.instructure.student.mobius.syllabus.SyllabusEvent import com.instructure.student.router.RouteMatcher @@ -170,8 +169,8 @@ class SyllabusView( } } - fun showAssignmentView(assignment: Assignment, canvasContext: CanvasContext) { - RouteMatcher.route(activity as FragmentActivity, AssignmentDetailsFragment.makeRoute(canvasContext, assignment.id)) + fun showAssignmentView(assignmentId: Long, canvasContext: CanvasContext) { + RouteMatcher.route(activity as FragmentActivity, AssignmentDetailsFragment.makeRoute(canvasContext, assignmentId)) } fun showScheduleItemView(scheduleItem: ScheduleItem, canvasContext: CanvasContext) { diff --git a/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt b/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt index 36a68b4187..4b9c97bf8f 100644 --- a/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt +++ b/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt @@ -401,6 +401,13 @@ object RouteMatcher : BaseRouteMatcher() { ) ) + // Studio Media Immersive View + routes.add( + Route( + "/media_attachments/:${RouterParams.ATTACHMENT_ID}/immersive_view", + InternalWebviewFragment::class.java + ) + ) // Submissions // :sliding_tab_type can be /rubric or /submissions (used to navigate to the nested fragment) @@ -538,6 +545,31 @@ object RouteMatcher : BaseRouteMatcher() { // No route, no problem handleWebViewUrl(activity, route.uri.toString()) } + } else if (route.primaryClass == InternalWebviewFragment::class.java && route.uri?.toString()?.contains("media_attachments") == true) { + // Handle studio media immersive view - pass the full URL and title to InternalWebviewFragment + val uri = route.uri!! + var urlString = uri.toString() + + // Convert media_attachments_iframe to media_attachments (for iframe button) + urlString = urlString.replace("media_attachments_iframe", "media_attachments") + + // Ensure embedded=true parameter is always present + if (!urlString.contains("embedded=true")) { + val separator = if (urlString.contains("?")) "&" else "?" + urlString = "$urlString${separator}embedded=true" + } + + route.arguments.putString(Const.INTERNAL_URL, urlString) + + // Extract title from URL query parameter if present, otherwise use fallback + val title = uri.getQueryParameter("title") ?: activity.getString(R.string.immersiveView) + route.arguments.putString(Const.ACTION_BAR_TITLE, title) + + if (activity.resources.getBoolean(R.bool.isDeviceTablet)) { + handleTabletRoute(activity, route) + } else { + handleFullscreenRoute(activity, route) + } } else if (route.routeContext == RouteContext.FILE || route.primaryClass?.isAssignableFrom(FileListFragment::class.java) == true && route.queryParamsHash.containsKey(RouterParams.PREVIEW) diff --git a/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt b/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt index 8010db1157..ab26515598 100644 --- a/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt +++ b/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt @@ -21,7 +21,6 @@ import android.webkit.WebView import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.ContextCompat import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.instructure.pandautils.utils.filecache.FileCache import com.instructure.canvasapi2.utils.Analytics import com.instructure.canvasapi2.utils.AnalyticsEventConstants import com.instructure.canvasapi2.utils.Logger @@ -32,14 +31,13 @@ import com.instructure.pandautils.utils.AppTheme import com.instructure.pandautils.utils.AppType import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.filecache.FileCache import com.instructure.student.BuildConfig import com.instructure.student.R import com.instructure.student.activity.NavigationActivity -import com.pspdfkit.PSPDFKit -import com.pspdfkit.exceptions.InvalidPSPDFKitLicenseException -import com.pspdfkit.exceptions.PSPDFKitInitializationFailedException -import com.pspdfkit.initialization.InitializationOptions -import com.zynksoftware.documentscanner.ui.DocumentScanner +import com.pspdfkit.Nutrient +import com.pspdfkit.exceptions.InvalidNutrientLicenseException +import com.pspdfkit.exceptions.NutrientInitializationFailedException abstract class BaseAppManager : com.instructure.canvasapi2.AppManager(), AnalyticsEventHandling { @@ -61,9 +59,7 @@ abstract class BaseAppManager : com.instructure.canvasapi2.AppManager(), Analyti // Hold off on initializing this until we emit the user properties. RemoteConfigUtils.initialize() - initPSPDFKit() - - initDocumentScanning() + initNutrient() if (BuildConfig.DEBUG) { FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false) @@ -110,19 +106,15 @@ abstract class BaseAppManager : com.instructure.canvasapi2.AppManager(), Analyti } - private fun initPSPDFKit() { + private fun initNutrient() { try { - PSPDFKit.initialize(this, InitializationOptions(licenseKey = BuildConfig.PSPDFKIT_LICENSE_KEY)) - } catch (e: PSPDFKitInitializationFailedException) { - Logger.e("Current device is not compatible with PSPDFKIT!") - } catch (e: InvalidPSPDFKitLicenseException) { - Logger.e("Invalid or Trial PSPDFKIT License!") + Nutrient.initialize(this, BuildConfig.PSPDFKIT_LICENSE_KEY) + } catch (e: NutrientInitializationFailedException) { + Logger.e("Current device is not compatible with Nutrient!") + } catch (e: InvalidNutrientLicenseException) { + Logger.e("Invalid or Trial Nutrient License!") } } - private fun initDocumentScanning() { - DocumentScanner.init(this) - } - override fun performLogoutOnAuthError() = Unit } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt b/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt index 73fb0b1066..1e34d0599f 100644 --- a/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt +++ b/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt @@ -72,20 +72,14 @@ object FileUtils { // We don't want to allow users to edit for submission viewing pspdfActivityConfiguration = PdfActivityConfiguration.Builder(context) .scrollDirection(PageScrollDirection.HORIZONTAL) - .showThumbnailGrid() .setThumbnailBarMode(ThumbnailBarMode.THUMBNAIL_BAR_MODE_PINNED) - .disableAnnotationEditing() - .disableAnnotationList() - .disableDocumentEditor() .fitMode(PageFitMode.FIT_TO_WIDTH) .build() } else { // Standard behavior pspdfActivityConfiguration = PdfActivityConfiguration.Builder(context) .scrollDirection(PageScrollDirection.HORIZONTAL) - .showThumbnailGrid() .setDocumentInfoViewSeparated(false) - .enableDocumentEditor() .enabledAnnotationTools(annotationCreationList) .editableAnnotationTypes(annotationEditList) .fitMode(PageFitMode.FIT_TO_WIDTH) diff --git a/apps/student/src/main/java/com/instructure/student/widget/todo/ToDoWidgetRepository.kt b/apps/student/src/main/java/com/instructure/student/widget/todo/ToDoWidgetRepository.kt index 3a101244e2..107090320a 100644 --- a/apps/student/src/main/java/com/instructure/student/widget/todo/ToDoWidgetRepository.kt +++ b/apps/student/src/main/java/com/instructure/student/widget/todo/ToDoWidgetRepository.kt @@ -49,6 +49,7 @@ class ToDoWidgetRepository( startDate, endDate, contextCodes, + null, restParams ).depaginate { plannerApi.nextPagePlannerItems(it, restParams) diff --git a/apps/student/src/main/res/layout/activity_document_scanning.xml b/apps/student/src/main/res/layout/activity_document_scanning.xml deleted file mode 100644 index a8b7347a77..0000000000 --- a/apps/student/src/main/res/layout/activity_document_scanning.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/student/src/main/res/layout/activity_navigation.xml b/apps/student/src/main/res/layout/activity_navigation.xml index d508f631e4..cdbdefd661 100644 --- a/apps/student/src/main/res/layout/activity_navigation.xml +++ b/apps/student/src/main/res/layout/activity_navigation.xml @@ -61,6 +61,7 @@ android:id="@+id/bottomBar" android:layout_width="match_parent" android:layout_height="wrap_content" + android:minHeight="48dp" android:background="@color/backgroundLightestElevated" app:elevation="0dp" app:itemIconTint="@color/textDarkest" diff --git a/apps/student/src/main/res/layout/fragment_picker_submission_upload.xml b/apps/student/src/main/res/layout/fragment_picker_submission_upload.xml index 96be4e68e1..73b14a4553 100644 --- a/apps/student/src/main/res/layout/fragment_picker_submission_upload.xml +++ b/apps/student/src/main/res/layout/fragment_picker_submission_upload.xml @@ -160,36 +160,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/student/src/main/res/layout/viewholder_discussion.xml b/apps/student/src/main/res/layout/viewholder_discussion.xml index ccccfd5942..366e27a3e9 100644 --- a/apps/student/src/main/res/layout/viewholder_discussion.xml +++ b/apps/student/src/main/res/layout/viewholder_discussion.xml @@ -63,6 +63,17 @@ android:layout_toStartOf="@+id/discussionOverflow" tools:text="Beginning of the Biological Existence of Mankind in the Jungles of South Asia Hodor Hodor" /> + + diff --git a/apps/student/src/main/res/layout/viewholder_module.xml b/apps/student/src/main/res/layout/viewholder_module.xml index bb75dcb69c..6b9c81b20d 100644 --- a/apps/student/src/main/res/layout/viewholder_module.xml +++ b/apps/student/src/main/res/layout/viewholder_module.xml @@ -71,25 +71,26 @@ style="@style/AdapterItemDescriptionText" tools:text="An assignment description with some length so we can ensure it looks amazing on every device!" /> - - - + tools:text="Due Feb 12 at 11:59 PM"/> - + - + diff --git a/apps/student/src/main/res/layout/viewholder_notification.xml b/apps/student/src/main/res/layout/viewholder_notification.xml index 9f0eeef524..b1ac6c257a 100644 --- a/apps/student/src/main/res/layout/viewholder_notification.xml +++ b/apps/student/src/main/res/layout/viewholder_notification.xml @@ -40,6 +40,11 @@ style="@style/AdapterItemTextContainer" android:orientation="vertical"> + + + android:id="@+id/checkpointLabel" + style="@style/AdapterItemDescriptionText" + android:textAlignment="viewStart" + android:visibility="gone" + tools:text="Reply to topic" /> (relaxed = true) private val moduleFacade = mockk(relaxed = true) - private val courseSettingsDao: CourseSettingsDao = mockk(relaxed = true) + private val courseSettingsDao = mockk(relaxed = true) + private val checkpointDao = mockk(relaxed = true) - private val dataSource = ModuleListLocalDataSource(tabDao, moduleFacade, courseSettingsDao) + private val dataSource = ModuleListLocalDataSource(tabDao, moduleFacade, courseSettingsDao, checkpointDao) @Test fun `getAllModuleObjects returns all module objects with DB api type`() = runTest { @@ -99,4 +103,100 @@ class ModuleListLocalDataSourceTest { assertEquals(expected, result) } + + @Test + fun `Get module item checkpoints groups by module item id`() = runTest { + val checkpointEntities = listOf( + CheckpointEntity( + id = 1, + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 5.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ), + CheckpointEntity( + id = 2, + assignmentId = null, + name = null, + tag = "reply_to_entry", + pointsPossible = 5.0, + dueAt = "2025-10-20T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + ) + coEvery { checkpointDao.findByCourseIdWithModuleItem(any()) } returns checkpointEntities + + val result = dataSource.getModuleItemCheckpoints("1", false) + + assertEquals(1, result.size) + assertEquals("100", result[0].moduleItemId) + assertEquals(2, result[0].checkpoints.size) + } + + @Test + fun `Get module item checkpoints filters out null module item ids`() = runTest { + val checkpointEntities = listOf( + CheckpointEntity( + id = 1, + assignmentId = 1L, + name = null, + tag = "reply_to_topic", + pointsPossible = 5.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = null, + courseId = 1L + ) + ) + coEvery { checkpointDao.findByCourseIdWithModuleItem(any()) } returns checkpointEntities + + val result = dataSource.getModuleItemCheckpoints("1", false) + + assertTrue(result.isEmpty()) + } + + @Test + fun `Get module item checkpoints returns empty list when no checkpoints found`() = runTest { + coEvery { checkpointDao.findByCourseIdWithModuleItem(any()) } returns emptyList() + + val result = dataSource.getModuleItemCheckpoints("1", false) + + assertTrue(result.isEmpty()) + } + + @Test + fun `Get module item checkpoints converts checkpoint entities to api models`() = runTest { + val checkpointEntity = CheckpointEntity( + id = 1, + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + coEvery { checkpointDao.findByCourseIdWithModuleItem(any()) } returns listOf(checkpointEntity) + + val result = dataSource.getModuleItemCheckpoints("1", false) + + assertEquals(1, result.size) + assertEquals("reply_to_topic", result[0].checkpoints[0].tag) + assertEquals(10.0, result[0].checkpoints[0].pointsPossible, 0.01) + } } \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSourceTest.kt index e1466f82d8..2aa7d4eaaa 100644 --- a/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSourceTest.kt +++ b/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSourceTest.kt @@ -19,6 +19,9 @@ package com.instructure.student.features.modules.list.datasource import com.instructure.canvasapi2.apis.CourseAPI import com.instructure.canvasapi2.apis.ModuleAPI import com.instructure.canvasapi2.apis.TabAPI +import com.instructure.canvasapi2.managers.graphql.ModuleItemCheckpoint +import com.instructure.canvasapi2.managers.graphql.ModuleItemWithCheckpoints +import com.instructure.canvasapi2.managers.graphql.ModuleManager import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.ModuleItem @@ -37,8 +40,9 @@ class ModuleListNetworkDataSourceTest { private val moduleApi: ModuleAPI.ModuleInterface = mockk(relaxed = true) private val tabApi: TabAPI.TabsInterface = mockk(relaxed = true) private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val moduleManager: ModuleManager = mockk(relaxed = true) - private val dataSource = ModuleListNetworkDataSource(moduleApi, tabApi, courseApi) + private val dataSource = ModuleListNetworkDataSource(moduleApi, tabApi, courseApi, moduleManager) @Test fun `Return failed result when getAllModuleObjects fails`() = runTest { @@ -183,4 +187,33 @@ class ModuleListNetworkDataSourceTest { Assert.assertNull(result) } + + @Test + fun `Get module item checkpoints returns data from module manager`() = runTest { + val expectedCheckpoints = listOf( + ModuleItemWithCheckpoints( + "1", + listOf( + ModuleItemCheckpoint(null, "reply_to_topic", 5.0), + ModuleItemCheckpoint(null, "reply_to_entry", 5.0) + ) + ) + ) + coEvery { moduleManager.getModuleItemCheckpoints(any(), any()) } returns expectedCheckpoints + + val result = dataSource.getModuleItemCheckpoints("123", true) + + Assert.assertEquals(1, result.size) + Assert.assertEquals("1", result[0].moduleItemId) + Assert.assertEquals(2, result[0].checkpoints.size) + } + + @Test + fun `Get module item checkpoints returns empty list when no checkpoints available`() = runTest { + coEvery { moduleManager.getModuleItemCheckpoints(any(), any()) } returns emptyList() + + val result = dataSource.getModuleItemCheckpoints("123", true) + + Assert.assertTrue(result.isEmpty()) + } } \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/test/adapter/TodoListRecyclerAdapterTest.kt b/apps/student/src/test/java/com/instructure/student/test/adapter/TodoListRecyclerAdapterTest.kt new file mode 100644 index 0000000000..082a4eca56 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/test/adapter/TodoListRecyclerAdapterTest.kt @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.test.adapter + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.apis.PlannerAPI +import com.instructure.canvasapi2.models.Plannable +import com.instructure.canvasapi2.models.PlannableType +import com.instructure.canvasapi2.models.PlannerItem +import com.instructure.canvasapi2.models.SubmissionState +import com.instructure.student.adapter.TodoListRecyclerAdapter +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Date + +@RunWith(AndroidJUnit4::class) +class TodoListRecyclerAdapterTest { + + private lateinit var adapter: TodoListRecyclerAdapter + + class TestTodoListRecyclerAdapter(context: Context) : TodoListRecyclerAdapter(context) + + @Before + fun setup() { + adapter = TestTodoListRecyclerAdapter(ApplicationProvider.getApplicationContext()) + } + + @Test + fun `isComplete returns true for submitted assignment`() { + val plannerItem = createPlannerItem( + plannableType = PlannableType.ASSIGNMENT, + submissionState = SubmissionState(submitted = true) + ) + + assertTrue(adapter.isComplete(plannerItem)) + } + + @Test + fun `isComplete returns false for unsubmitted assignment`() { + val plannerItem = createPlannerItem( + plannableType = PlannableType.ASSIGNMENT, + submissionState = SubmissionState(submitted = false) + ) + + assertEquals(false, adapter.isComplete(plannerItem)) + } + + @Test + fun `isComplete returns true for submitted discussion topic`() { + val plannerItem = createPlannerItem( + plannableType = PlannableType.DISCUSSION_TOPIC, + submissionState = SubmissionState(submitted = true) + ) + + assertTrue(adapter.isComplete(plannerItem)) + } + + @Test + fun `isComplete returns false for unsubmitted discussion topic`() { + val plannerItem = createPlannerItem( + plannableType = PlannableType.DISCUSSION_TOPIC, + submissionState = SubmissionState(submitted = false) + ) + + assertEquals(false, adapter.isComplete(plannerItem)) + } + + @Test + fun `isComplete returns true for submitted sub assignment`() { + val plannerItem = createPlannerItem( + plannableType = PlannableType.SUB_ASSIGNMENT, + submissionState = SubmissionState(submitted = true) + ) + + assertTrue(adapter.isComplete(plannerItem)) + } + + @Test + fun `isComplete returns false for unsubmitted sub assignment`() { + val plannerItem = createPlannerItem( + plannableType = PlannableType.SUB_ASSIGNMENT, + submissionState = SubmissionState(submitted = false) + ) + + assertEquals(false, adapter.isComplete(plannerItem)) + } + + @Test + fun `isComplete returns true for submitted quiz`() { + val plannerItem = createPlannerItem( + plannableType = PlannableType.QUIZ, + submissionState = SubmissionState(submitted = true) + ) + + assertTrue(adapter.isComplete(plannerItem)) + } + + @Test + fun `isComplete returns false for unsubmitted quiz`() { + val plannerItem = createPlannerItem( + plannableType = PlannableType.QUIZ, + submissionState = SubmissionState(submitted = false) + ) + + assertEquals(false, adapter.isComplete(plannerItem)) + } + + @Test + fun `isComplete returns false for quiz with null submission state`() { + val plannerItem = createPlannerItem( + plannableType = PlannableType.QUIZ, + submissionState = null + ) + + assertEquals(false, adapter.isComplete(plannerItem)) + } + + @Test + fun `isComplete returns false for calendar event`() { + val plannerItem = createPlannerItem( + plannableType = PlannableType.CALENDAR_EVENT, + submissionState = SubmissionState(submitted = true) + ) + + assertEquals(false, adapter.isComplete(plannerItem)) + } + + @Test + fun `isComplete returns false for planner note`() { + val plannerItem = createPlannerItem( + plannableType = PlannableType.PLANNER_NOTE, + submissionState = SubmissionState(submitted = true) + ) + + assertEquals(false, adapter.isComplete(plannerItem)) + } + + @Test + fun `isComplete returns false for wiki page`() { + val plannerItem = createPlannerItem( + plannableType = PlannableType.WIKI_PAGE, + submissionState = SubmissionState(submitted = true) + ) + + assertEquals(false, adapter.isComplete(plannerItem)) + } + + @Test + fun `isComplete returns false for todo`() { + val plannerItem = createPlannerItem( + plannableType = PlannableType.TODO, + submissionState = SubmissionState(submitted = true) + ) + + assertEquals(false, adapter.isComplete(plannerItem)) + } + + private fun createPlannerItem( + plannableType: PlannableType, + submissionState: SubmissionState? = null + ): PlannerItem { + val plannable = Plannable( + id = 1L, + title = "Test Item", + courseId = 1L, + groupId = null, + userId = null, + pointsPossible = null, + dueAt = null, + assignmentId = 1L, + todoDate = null, + startAt = null, + endAt = null, + details = null, + allDay = null + ) + + return PlannerItem( + courseId = 1L, + groupId = null, + userId = null, + contextType = "Course", + contextName = "Test Course", + plannableType = plannableType, + plannable = plannable, + plannableDate = Date(), + htmlUrl = "/courses/1", + submissionState = submissionState, + newActivity = false, + plannerOverride = null, + plannableItemDetails = null, + isChecked = false + ) + } +} diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/PickerSubmissionUploadUpdateTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/PickerSubmissionUploadUpdateTest.kt index 08811ab839..94c79d1652 100644 --- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/PickerSubmissionUploadUpdateTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/PickerSubmissionUploadUpdateTest.kt @@ -73,7 +73,7 @@ class PickerSubmissionUploadUpdateTest : Assert() { fun `Initializes with LoadFileContents effect given a media uri`() { val uri = mockk() val startModel = initModel.copy(mediaFileUri = uri, isLoadingFile = false) - val expectedModel = startModel.copy(isLoadingFile = true) + val expectedModel = startModel.copy(isLoadingFile = true, mediaSource = "camera") initSpec .whenInit(startModel) .then( diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/TextSubmissionUploadEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/TextSubmissionUploadEffectHandlerTest.kt index 7ab6c15e40..dc4c298452 100644 --- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/TextSubmissionUploadEffectHandlerTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/TextSubmissionUploadEffectHandlerTest.kt @@ -58,10 +58,10 @@ class TextSubmissionUploadEffectHandlerTest : Assert() { val assignmentName = "Name" val course = Course() - connection.accept(TextSubmissionUploadEffect.SubmitText(text, course, assignmentId, assignmentName)) + connection.accept(TextSubmissionUploadEffect.SubmitText(text, course, assignmentId, assignmentName, 1L)) verify(timeout = 100) { - submissionHelper.startTextSubmission(course, assignmentId, assignmentName, text) + submissionHelper.startTextSubmission(course, assignmentId, assignmentName, text, 1L) view.goBack() } diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/TextSubmissionUploadUpdateTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/TextSubmissionUploadUpdateTest.kt index be9b0ba39b..b64a39c479 100644 --- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/TextSubmissionUploadUpdateTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/TextSubmissionUploadUpdateTest.kt @@ -117,7 +117,8 @@ class TextSubmissionUploadUpdateTest : Assert() { text, course, assignment.id, - assignment.name + assignment.name, + 1L ) ) ) @@ -138,7 +139,8 @@ class TextSubmissionUploadUpdateTest : Assert() { text, course, assignment.id, - assignment.name + assignment.name, + 1L ) ) ) diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/UrlSubmissionUploadEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/UrlSubmissionUploadEffectHandlerTest.kt index 0c1e3452db..a6e06cd0d8 100644 --- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/UrlSubmissionUploadEffectHandlerTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/UrlSubmissionUploadEffectHandlerTest.kt @@ -66,10 +66,10 @@ class UrlSubmissionUploadEffectHandlerTest : Assert() { val course = Course() val url = "www.instructure.com" - connection.accept(UrlSubmissionUploadEffect.SubmitUrl(url, course, assignment.id, assignment.name)) + connection.accept(UrlSubmissionUploadEffect.SubmitUrl(url, course, assignment.id, assignment.name, 1L)) verify(timeout = 100) { - submissionHelper.startUrlSubmission(course, assignment.id, assignment.name, url) + submissionHelper.startUrlSubmission(course, assignment.id, assignment.name, url, 1L) } confirmVerified(submissionHelper) diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/UrlSubmissionUploadUpdateTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/UrlSubmissionUploadUpdateTest.kt index c18219605c..0e009dfe16 100644 --- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/UrlSubmissionUploadUpdateTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/UrlSubmissionUploadUpdateTest.kt @@ -155,7 +155,7 @@ class UrlSubmissionUploadUpdateTest : Assert() { @Test fun `SubmitClicked event with valid url results in SubmitUrl effect`() { - val expectedEffect: UrlSubmissionUploadEffect = UrlSubmissionUploadEffect.SubmitUrl(defaultValidUrl, course, assignment.id, assignment.name) + val expectedEffect: UrlSubmissionUploadEffect = UrlSubmissionUploadEffect.SubmitUrl(defaultValidUrl, course, assignment.id, assignment.name, 1L) updateSpec .given(initModel) diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEmptyContentEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEmptyContentEffectHandlerTest.kt index fe6cb957fa..6dfa3f0353 100644 --- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEmptyContentEffectHandlerTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEmptyContentEffectHandlerTest.kt @@ -258,7 +258,8 @@ class SubmissionDetailsEmptyContentEffectHandlerTest : Assert() { assignment.id, assignment.name, assignment.groupCategoryId, - "Path" + "Path", + mediaSource = "audio_recorder" ) } returns Unit @@ -270,7 +271,8 @@ class SubmissionDetailsEmptyContentEffectHandlerTest : Assert() { assignment.id, assignment.name, assignment.groupCategoryId, - "Path" + "Path", + mediaSource = "audio_recorder" ) } } diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusEffectHandlerTest.kt index fe6928bb8c..b1b0abf73a 100644 --- a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusEffectHandlerTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusEffectHandlerTest.kt @@ -96,7 +96,39 @@ class SyllabusEffectHandlerTest : Assert() { coEvery { syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.ASSIGNMENT, + any(), + any(), + any(), + any() + ) + } returns DataResult.Fail() + + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.SUB_ASSIGNMENT, + any(), any(), + any(), + any() + ) + } returns DataResult.Fail() + + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.CALENDAR, + any(), + any(), + any(), + any() + ) + } returns DataResult.Fail() + + coEvery { + syllabusRepository.getPlannerItems( any(), any(), any(), @@ -159,6 +191,17 @@ class SyllabusEffectHandlerTest : Assert() { ) } returns DataResult.Success(assignments) + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.SUB_ASSIGNMENT, + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(emptyList()) + coEvery { syllabusRepository.getCalendarEvents( any(), @@ -170,6 +213,16 @@ class SyllabusEffectHandlerTest : Assert() { ) } returns DataResult.Success(calendarEvents) + coEvery { + syllabusRepository.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(emptyList()) + connection.accept(SyllabusEffect.LoadData(courseId, false)) verify(timeout = 100) { @@ -206,6 +259,17 @@ class SyllabusEffectHandlerTest : Assert() { ) } returns DataResult.Success(assignments) + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.SUB_ASSIGNMENT, + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(emptyList()) + coEvery { syllabusRepository.getCalendarEvents( any(), @@ -217,6 +281,16 @@ class SyllabusEffectHandlerTest : Assert() { ) } returns DataResult.Fail() + coEvery { + syllabusRepository.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(emptyList()) + connection.accept(SyllabusEffect.LoadData(courseId, false)) verify(timeout = 100) { @@ -253,6 +327,17 @@ class SyllabusEffectHandlerTest : Assert() { ) } returns DataResult.Fail() + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.SUB_ASSIGNMENT, + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(emptyList()) + coEvery { syllabusRepository.getCalendarEvents( any(), @@ -264,6 +349,16 @@ class SyllabusEffectHandlerTest : Assert() { ) } returns DataResult.Success(calendarEvents) + coEvery { + syllabusRepository.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(emptyList()) + connection.accept(SyllabusEffect.LoadData(courseId, false)) verify(timeout = 100) { @@ -307,6 +402,53 @@ class SyllabusEffectHandlerTest : Assert() { coEvery { syllabusRepository.getCourseWithSyllabus(courseId, false) } returns DataResult.Success(course) + connection.accept(SyllabusEffect.LoadData(courseId, false)) + + verify(timeout = 100) { + eventConsumer.accept(expectedEvent) + } + + confirmVerified(eventConsumer) + } + + @Test + fun `LoadData with sub-assignments results in DataLoaded with sorted items`() { + val courseId = 1L + val itemCount = 2 + val now = Date().time + val assignments = List(itemCount) { + ScheduleItem( + itemId = it.toString(), + itemType = ScheduleItem.Type.TYPE_ASSIGNMENT, + startAt = Date(now + (1000 * it)).toApiString() + ) + } + val subAssignments = List(itemCount) { + ScheduleItem( + itemId = (it + assignments.size).toString(), + itemType = ScheduleItem.Type.TYPE_ASSIGNMENT, + startAt = Date(now + (500 * it)).toApiString() + ) + } + val calendarEvents = List(itemCount) { + ScheduleItem( + itemId = (it + assignments.size + subAssignments.size).toString(), + itemType = ScheduleItem.Type.TYPE_CALENDAR, + startAt = Date(now + (1000 * it)).toApiString() + ) + } + + val allItems = (assignments + subAssignments + calendarEvents).sortedBy { it.startAt } + + val expectedEvent = SyllabusEvent.DataLoaded( + DataResult.Success(course), + DataResult.Success(allItems) + ) + + coEvery { syllabusRepository.getCourseWithSyllabus(any(), any()) } returns DataResult.Success(course) + + coEvery { syllabusRepository.getCourseSettings(any(), any()) } returns CourseSettings(courseSummary = true) + coEvery { syllabusRepository.getCalendarEvents( any(), @@ -318,6 +460,17 @@ class SyllabusEffectHandlerTest : Assert() { ) } returns DataResult.Success(assignments) + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.SUB_ASSIGNMENT, + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(subAssignments) + coEvery { syllabusRepository.getCalendarEvents( any(), @@ -329,6 +482,84 @@ class SyllabusEffectHandlerTest : Assert() { ) } returns DataResult.Success(calendarEvents) + coEvery { + syllabusRepository.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(emptyList()) + + connection.accept(SyllabusEffect.LoadData(courseId, false)) + + verify(timeout = 100) { + eventConsumer.accept(expectedEvent) + } + + confirmVerified(eventConsumer) + } + + @Test + fun `LoadData with failed sub-assignments results in partial success DataLoaded`() { + val courseId = 1L + val assignments = List(1) { + ScheduleItem(itemId = it.toString(), itemType = ScheduleItem.Type.TYPE_ASSIGNMENT) + } + + val expectedEvent = SyllabusEvent.DataLoaded( + DataResult.Success(course), + DataResult.Success(assignments) + ) + + coEvery { syllabusRepository.getCourseWithSyllabus(courseId, false) } returns DataResult.Success(course) + + coEvery { syllabusRepository.getCourseSettings(courseId, false) } returns CourseSettings(courseSummary = true) + + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.ASSIGNMENT, + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(assignments) + + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.SUB_ASSIGNMENT, + any(), + any(), + any(), + any() + ) + } returns DataResult.Fail() + + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.CALENDAR, + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(emptyList()) + + coEvery { + syllabusRepository.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(emptyList()) + connection.accept(SyllabusEffect.LoadData(courseId, false)) verify(timeout = 100) { @@ -341,10 +572,10 @@ class SyllabusEffectHandlerTest : Assert() { @Test fun `ShowAssignmentView results in view calling showAssignmentView`() { val assignment = Assignment() - connection.accept(SyllabusEffect.ShowAssignmentView(assignment, course)) + connection.accept(SyllabusEffect.ShowAssignmentView(assignment.id, course)) verify(timeout = 100) { - view.showAssignmentView(assignment, course) + view.showAssignmentView(assignment.id, course) } confirmVerified(view) diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusLocalDataSourceTest.kt index 46fa94252d..82d63d6fd5 100644 --- a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusLocalDataSourceTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusLocalDataSourceTest.kt @@ -21,12 +21,18 @@ package com.instructure.student.test.syllabus import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.Plannable +import com.instructure.canvasapi2.models.PlannableType +import com.instructure.canvasapi2.models.PlannerItem import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.daos.PlannerItemDao import com.instructure.pandautils.room.offline.entities.CourseSettingsEntity +import com.instructure.pandautils.room.offline.entities.PlannerItemEntity import com.instructure.pandautils.room.offline.facade.CourseFacade import com.instructure.pandautils.room.offline.facade.ScheduleItemFacade +import java.util.Date import com.instructure.student.mobius.syllabus.datasource.SyllabusLocalDataSource import io.mockk.coEvery import io.mockk.coVerify @@ -41,12 +47,13 @@ class SyllabusLocalDataSourceTest { private val courseSettingsDao: CourseSettingsDao = mockk(relaxed = true) private val courseFacade: CourseFacade = mockk(relaxed = true) private val scheduleItemFacade: ScheduleItemFacade = mockk(relaxed = true) + private val plannerItemDao: PlannerItemDao = mockk(relaxed = true) private lateinit var syllabusLocalDataSource: SyllabusLocalDataSource @Before fun setup() { - syllabusLocalDataSource = SyllabusLocalDataSource(courseSettingsDao, courseFacade, scheduleItemFacade) + syllabusLocalDataSource = SyllabusLocalDataSource(courseSettingsDao, courseFacade, scheduleItemFacade, plannerItemDao) } @Test @@ -120,4 +127,148 @@ class SyllabusLocalDataSourceTest { assertEquals(DataResult.Fail(), result) } + + @Test + fun `Return planner items for single course`() = runTest { + val plannable = Plannable( + id = 1L, + title = "Assignment 1", + courseId = 1L, + groupId = null, + userId = null, + pointsPossible = 10.0, + dueAt = Date(), + assignmentId = 1L, + todoDate = null, + startAt = null, + endAt = null, + details = "Assignment details", + allDay = false + ) + val plannerItem = PlannerItem( + courseId = 1L, + groupId = null, + userId = null, + contextType = "course", + contextName = "Course 1", + plannableType = PlannableType.ASSIGNMENT, + plannable = plannable, + plannableDate = Date(), + htmlUrl = "https://example.com", + submissionState = null, + newActivity = false + ) + val entity = PlannerItemEntity(plannerItem, 1L) + val expected = listOf(plannerItem) + + coEvery { plannerItemDao.findByCourseIds(listOf(1L)) } returns listOf(entity) + + val result = syllabusLocalDataSource.getPlannerItems(null, null, listOf("course_1"), null, false) + + assertEquals(DataResult.Success(expected).isSuccess, result.isSuccess) + coVerify(exactly = 1) { + plannerItemDao.findByCourseIds(listOf(1L)) + } + } + + @Test + fun `Return planner items for multiple courses`() = runTest { + val plannable1 = Plannable( + id = 1L, + title = "Assignment 1", + courseId = 1L, + groupId = null, + userId = null, + pointsPossible = 10.0, + dueAt = Date(), + assignmentId = 1L, + todoDate = null, + startAt = null, + endAt = null, + details = "Assignment details", + allDay = false + ) + val plannerItem1 = PlannerItem( + courseId = 1L, + groupId = null, + userId = null, + contextType = "course", + contextName = "Course 1", + plannableType = PlannableType.ASSIGNMENT, + plannable = plannable1, + plannableDate = Date(), + htmlUrl = "https://example.com", + submissionState = null, + newActivity = false + ) + val entity1 = PlannerItemEntity(plannerItem1, 1L) + + val plannable2 = Plannable( + id = 2L, + title = "Assignment 2", + courseId = 2L, + groupId = null, + userId = null, + pointsPossible = 10.0, + dueAt = Date(), + assignmentId = 2L, + todoDate = null, + startAt = null, + endAt = null, + details = "Assignment details", + allDay = false + ) + val plannerItem2 = PlannerItem( + courseId = 2L, + groupId = null, + userId = null, + contextType = "course", + contextName = "Course 2", + plannableType = PlannableType.ASSIGNMENT, + plannable = plannable2, + plannableDate = Date(), + htmlUrl = "https://example.com", + submissionState = null, + newActivity = false + ) + val entity2 = PlannerItemEntity(plannerItem2, 2L) + + coEvery { plannerItemDao.findByCourseIds(listOf(1L, 2L)) } returns listOf(entity1, entity2) + + val result = syllabusLocalDataSource.getPlannerItems(null, null, listOf("course_1", "course_2"), null, false) + + assertEquals(DataResult.Success(listOf(plannerItem1, plannerItem2)).isSuccess, result.isSuccess) + coVerify(exactly = 1) { + plannerItemDao.findByCourseIds(listOf(1L, 2L)) + } + } + + @Test + fun `Return empty list when no context codes provided`() = runTest { + val result = syllabusLocalDataSource.getPlannerItems(null, null, emptyList(), null, false) + + assertEquals(DataResult.Success(emptyList()), result) + coVerify(exactly = 0) { + plannerItemDao.findByCourseIds(any()) + } + } + + @Test + fun `Return empty list for invalid context codes`() = runTest { + val result = syllabusLocalDataSource.getPlannerItems(null, null, listOf("user_1", "group_2"), null, false) + + assertEquals(DataResult.Success(emptyList()), result) + coVerify(exactly = 0) { + plannerItemDao.findByCourseIds(any()) + } + } + + @Test + fun `Return failed data result on planner items error`() = runTest { + coEvery { plannerItemDao.findByCourseIds(any()) } throws Exception() + + val result = syllabusLocalDataSource.getPlannerItems(null, null, listOf("course_1"), null, false) + + assertEquals(DataResult.Fail(), result) + } } \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusNetworkDataSourceTest.kt index 6f926884be..94cee5dfd4 100644 --- a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusNetworkDataSourceTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusNetworkDataSourceTest.kt @@ -20,6 +20,7 @@ package com.instructure.student.test.syllabus import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.PlannerAPI import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings @@ -40,12 +41,13 @@ class SyllabusNetworkDataSourceTest { private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) private val calendarEventApi: CalendarEventAPI.CalendarEventInterface = mockk(relaxed = true) + private val plannerApi: PlannerAPI.PlannerInterface = mockk(relaxed = true) private lateinit var syllabusNetworkDataSource: SyllabusNetworkDataSource @Before fun setup() { - syllabusNetworkDataSource = SyllabusNetworkDataSource(courseApi, calendarEventApi) + syllabusNetworkDataSource = SyllabusNetworkDataSource(courseApi, calendarEventApi, plannerApi) } @Test diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusRepositoryTest.kt index dbff3a6049..b9500cb699 100644 --- a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusRepositoryTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusRepositoryTest.kt @@ -21,8 +21,12 @@ package com.instructure.student.test.syllabus import com.instructure.canvasapi2.apis.CalendarEventAPI import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.Plannable +import com.instructure.canvasapi2.models.PlannableType +import com.instructure.canvasapi2.models.PlannerItem import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult +import java.util.Date import com.instructure.pandautils.utils.FeatureFlagProvider import com.instructure.pandautils.utils.NetworkStateProvider import com.instructure.student.mobius.syllabus.SyllabusRepository @@ -227,4 +231,146 @@ class SyllabusRepositoryTest { assertEquals(DataResult.Fail(), result) } + + @Test + fun `Return planner items when online`() = runTest { + val plannable = Plannable( + id = 1L, + title = "Assignment 1", + courseId = 1L, + groupId = null, + userId = null, + pointsPossible = 10.0, + dueAt = Date(), + assignmentId = 1L, + todoDate = null, + startAt = null, + endAt = null, + details = "Assignment details", + allDay = false + ) + val expected = listOf( + PlannerItem( + courseId = 1L, + groupId = null, + userId = null, + contextType = "course", + contextName = "Course 1", + plannableType = PlannableType.ASSIGNMENT, + plannable = plannable, + plannableDate = Date(), + htmlUrl = "https://example.com", + submissionState = null, + newActivity = false + ) + ) + + every { networkStateProvider.isOnline() } returns true + + coEvery { + syllabusNetworkDataSource.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(expected) + + val result = repository.getPlannerItems(null, null, listOf("course_1"), null, false) + + assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { syllabusNetworkDataSource.getPlannerItems(null, null, listOf("course_1"), null, false) } + coVerify(exactly = 0) { syllabusLocalDataSource.getPlannerItems(any(), any(), any(), any(), any()) } + } + + @Test + fun `Return planner items when offline`() = runTest { + val plannable = Plannable( + id = 1L, + title = "Assignment 1", + courseId = 1L, + groupId = null, + userId = null, + pointsPossible = 10.0, + dueAt = Date(), + assignmentId = 1L, + todoDate = null, + startAt = null, + endAt = null, + details = "Assignment details", + allDay = false + ) + val expected = listOf( + PlannerItem( + courseId = 1L, + groupId = null, + userId = null, + contextType = "course", + contextName = "Course 1", + plannableType = PlannableType.ASSIGNMENT, + plannable = plannable, + plannableDate = Date(), + htmlUrl = "https://example.com", + submissionState = null, + newActivity = false + ) + ) + + every { networkStateProvider.isOnline() } returns false + + coEvery { + syllabusLocalDataSource.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(expected) + + val result = repository.getPlannerItems(null, null, listOf("course_1"), null, false) + + assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { syllabusLocalDataSource.getPlannerItems(null, null, listOf("course_1"), null, false) } + coVerify(exactly = 0) { syllabusNetworkDataSource.getPlannerItems(any(), any(), any(), any(), any()) } + } + + @Test + fun `Return failed result for planner items on network error`() = runTest { + every { networkStateProvider.isOnline() } returns true + + coEvery { + syllabusNetworkDataSource.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Fail() + + val result = repository.getPlannerItems(null, null, listOf("course_1"), null, false) + + assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return failed result for planner items on db error`() = runTest { + every { networkStateProvider.isOnline() } returns false + + coEvery { + syllabusLocalDataSource.getPlannerItems( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Fail() + + val result = repository.getPlannerItems(null, null, listOf("course_1"), null, false) + + assertEquals(DataResult.Fail(), result) + } } \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusUpdateTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusUpdateTest.kt index 3a9dfad3a4..25333100dc 100644 --- a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusUpdateTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusUpdateTest.kt @@ -19,6 +19,7 @@ import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.utils.orDefault import com.instructure.student.mobius.syllabus.SyllabusEffect import com.instructure.student.mobius.syllabus.SyllabusEvent import com.instructure.student.mobius.syllabus.SyllabusModel @@ -148,7 +149,7 @@ class SyllabusUpdateTest : Assert() { .whenEvent(SyllabusEvent.SyllabusItemClicked("0")) .then( assertThatNext( - matchesEffects(SyllabusEffect.ShowAssignmentView(events.data[0].assignment!!, course)) + matchesEffects(SyllabusEffect.ShowAssignmentView(events.data[0].assignment?.id.orDefault(), course)) ) ) } diff --git a/apps/student/src/test/java/com/instructure/student/widget/todo/ToDoWidgetRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/widget/todo/ToDoWidgetRepositoryTest.kt index e5f36044db..4781385a5b 100644 --- a/apps/student/src/test/java/com/instructure/student/widget/todo/ToDoWidgetRepositoryTest.kt +++ b/apps/student/src/test/java/com/instructure/student/widget/todo/ToDoWidgetRepositoryTest.kt @@ -49,7 +49,7 @@ class ToDoWidgetRepositoryTest { @Test fun `Returns failed result when planner api request fails`() = runTest { - coEvery { plannerApi.getPlannerItems(any(), any(), any(), any()) } returns DataResult.Fail() + coEvery { plannerApi.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Fail() val result = repository.getPlannerItems("2023-1-1", "2023-1-2", emptyList(), true) @@ -69,7 +69,7 @@ class ToDoWidgetRepositoryTest { createPlannerItem(2, 6, PlannableType.CALENDAR_EVENT) ) - coEvery { plannerApi.getPlannerItems(any(), any(), any(), any()) } returns DataResult.Success(plannerItems) + coEvery { plannerApi.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Success(plannerItems) val result = repository.getPlannerItems("2023-1-1", "2023-1-2", emptyList(), true) @@ -88,7 +88,7 @@ class ToDoWidgetRepositoryTest { createPlannerItem(2, 6, PlannableType.CALENDAR_EVENT) ) - coEvery { plannerApi.getPlannerItems(any(), any(), any(), any()) } returns DataResult.Success( + coEvery { plannerApi.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Success( plannerItems1, linkHeaders = LinkHeaders(nextUrl = "next") ) diff --git a/apps/teacher/build.gradle b/apps/teacher/build.gradle index e876ef6541..29113262da 100644 --- a/apps/teacher/build.gradle +++ b/apps/teacher/build.gradle @@ -16,7 +16,8 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' -apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-kapt' // Keep kapt for Data Binding +apply plugin: 'com.google.devtools.ksp' apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'org.jetbrains.kotlin.plugin.compose' @@ -32,13 +33,32 @@ android { exclude 'META-INF/rxjava.properties' } + packaging { + resources { + pickFirsts += [ + 'META-INF/INDEX.LIST', + 'META-INF/io.netty.versions.properties' + ] + excludes += [ + 'META-INF/DEPENDENCIES', + 'META-INF/LICENSE', + 'META-INF/LICENSE.txt', + 'META-INF/NOTICE', + 'META-INF/NOTICE.txt', + 'META-INF/maven/com.google.guava/guava/pom.properties', + 'META-INF/maven/com.google.guava/guava/pom.xml', + 'META-INF/rxjava.properties' + ] + } + } + defaultConfig { minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK versionCode = 82 versionName = '2.0.1' vectorDrawables.useSupportLibrary = true - testInstrumentationRunner 'com.instructure.teacher.ui.espresso.TeacherHiltTestRunner' + testInstrumentationRunner 'com.instructure.teacher.espresso.TeacherHiltTestRunner' testInstrumentationRunnerArguments disableAnalytics: 'true' /* BuildConfig fields */ @@ -262,6 +282,9 @@ dependencies { implementation Libs.ANDROIDX_VECTOR implementation Libs.PLAY_IN_APP_UPDATES + /* Analytics */ + implementation Libs.PENDO + /* Firebase */ implementation platform(Libs.FIREBASE_BOM) { exclude group: 'com.google.firebase', module: 'firebase-analytics' @@ -271,36 +294,28 @@ dependencies { transitive = true } - testImplementation Libs.ANDROIDX_CORE_TESTING + implementation Libs.CAMERA_VIEW - /* AAC */ - implementation Libs.VIEW_MODEL - implementation Libs.LIVE_DATA - implementation Libs.VIEW_MODE_SAVED_STATE - implementation Libs.ANDROIDX_FRAGMENT_KTX - kapt Libs.LIFECYCLE_COMPILER + testImplementation Libs.ANDROIDX_CORE_TESTING /* DI */ implementation Libs.HILT - kapt Libs.HILT_COMPILER + ksp Libs.HILT_COMPILER androidTestImplementation Libs.HILT_TESTING - kaptAndroidTestQa Libs.HILT_TESTING_COMPILER + kspAndroidTest Libs.HILT_TESTING_COMPILER implementation Libs.HILT_ANDROIDX_WORK - kapt Libs.HILT_ANDROIDX_COMPILER - - androidTestImplementation Libs.UI_AUTOMATOR - - /* WorkManager */ - implementation Libs.ANDROIDX_WORK_MANAGER - implementation Libs.ANDROIDX_WORK_MANAGER_KTX + ksp Libs.HILT_ANDROIDX_COMPILER - implementation Libs.PENDO - - implementation Libs.CAMERA_VIEW + /* AAC */ + implementation Libs.VIEW_MODEL + implementation Libs.LIVE_DATA + implementation Libs.VIEW_MODE_SAVED_STATE + implementation Libs.ANDROIDX_FRAGMENT_KTX + kapt Libs.LIFECYCLE_COMPILER // Keep kapt for lifecycle if it includes Data Binding - /* ROOM */ + /* Room */ implementation Libs.ROOM - kapt Libs.ROOM_COMPILER + ksp Libs.ROOM_COMPILER implementation Libs.ROOM_COROUTINES testImplementation Libs.HAMCREST diff --git a/apps/teacher/flank.yml b/apps/teacher/flank.yml index eafa4e03e4..a252da921a 100644 --- a/apps/teacher/flank.yml +++ b/apps/teacher/flank.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug device: - model: Pixel2.arm version: 29 diff --git a/apps/teacher/flank_coverage.yml b/apps/teacher/flank_coverage.yml index 3d23a78f48..d5415d3b04 100644 --- a/apps/teacher/flank_coverage.yml +++ b/apps/teacher/flank_coverage.yml @@ -19,7 +19,7 @@ gcloud: directories-to-pull: - /sdcard/ test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubCoverage + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubCoverage device: - model: Pixel2.arm version: 29 diff --git a/apps/teacher/flank_e2e.yml b/apps/teacher/flank_e2e.yml index 3734162948..eb5c5c77f7 100644 --- a/apps/teacher/flank_e2e.yml +++ b/apps/teacher/flank_e2e.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug device: - model: Pixel2.arm version: 29 diff --git a/apps/teacher/flank_e2e_coverage.yml b/apps/teacher/flank_e2e_coverage.yml index af3fe15179..17989f8cd2 100644 --- a/apps/teacher/flank_e2e_coverage.yml +++ b/apps/teacher/flank_e2e_coverage.yml @@ -19,8 +19,8 @@ gcloud: directories-to-pull: - /sdcard/ test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubCoverage + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubCoverage device: - model: Pixel2.arm version: 29 diff --git a/apps/teacher/flank_e2e_flaky.yml b/apps/teacher/flank_e2e_flaky.yml index 00402815a8..2653bb332c 100644 --- a/apps/teacher/flank_e2e_flaky.yml +++ b/apps/teacher/flank_e2e_flaky.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.FlakyE2E + - annotation com.instructure.canvas.espresso.annotations.FlakyE2E device: - model: Nexus6P version: 26 diff --git a/apps/teacher/flank_e2e_knownbug.yml b/apps/teacher/flank_e2e_knownbug.yml index a8bee4b7e0..fb7386683b 100644 --- a/apps/teacher/flank_e2e_knownbug.yml +++ b/apps/teacher/flank_e2e_knownbug.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.KnownBug + - annotation com.instructure.canvas.espresso.annotations.KnownBug device: - model: Nexus6P version: 26 diff --git a/apps/teacher/flank_e2e_lowres.yml b/apps/teacher/flank_e2e_lowres.yml index b6e24c1447..d9fa4b0dc8 100644 --- a/apps/teacher/flank_e2e_lowres.yml +++ b/apps/teacher/flank_e2e_lowres.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug device: - model: NexusLowRes version: 29 diff --git a/apps/teacher/flank_e2e_min.yml b/apps/teacher/flank_e2e_min.yml index 8137c96453..7ac548f08f 100644 --- a/apps/teacher/flank_e2e_min.yml +++ b/apps/teacher/flank_e2e_min.yml @@ -12,8 +12,8 @@ gcloud: record-video: true timeout: 60m test-targets: - - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - annotation com.instructure.canvas.espresso.annotations.E2E + - notAnnotation com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug device: - model: Nexus6P version: 26 diff --git a/apps/teacher/flank_landscape.yml b/apps/teacher/flank_landscape.yml index a2919c86ba..116542032a 100644 --- a/apps/teacher/flank_landscape.yml +++ b/apps/teacher/flank_landscape.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubLandscape + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubLandscape device: - model: Pixel2.arm version: 29 diff --git a/apps/teacher/flank_multi_api_level.yml b/apps/teacher/flank_multi_api_level.yml index 0b907030b7..282b5f0ee2 100644 --- a/apps/teacher/flank_multi_api_level.yml +++ b/apps/teacher/flank_multi_api_level.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubMultiAPILevel, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubMultiAPILevel, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug device: - model: NexusLowRes version: 27 diff --git a/apps/teacher/flank_tablet.yml b/apps/teacher/flank_tablet.yml index 6ec715a404..13224a18b7 100644 --- a/apps/teacher/flank_tablet.yml +++ b/apps/teacher/flank_tablet.yml @@ -9,7 +9,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubTablet + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.StubTablet device: - model: MediumTablet.arm version: 29 diff --git a/apps/teacher/proguard-rules.txt b/apps/teacher/proguard-rules.txt index 0157f11519..41f4c46430 100644 --- a/apps/teacher/proguard-rules.txt +++ b/apps/teacher/proguard-rules.txt @@ -258,4 +258,19 @@ -dontwarn java.beans.SimpleBeanInfo -keep class androidx.navigation.** { *; } - -keep interface androidx.navigation.** { *; } \ No newline at end of file + -keep interface androidx.navigation.** { *; } + +# Netty and BlockHound integration +-dontwarn reactor.blockhound.integration.BlockHoundIntegration +-dontwarn io.netty.util.internal.Hidden$NettyBlockHoundIntegration +-keep class reactor.blockhound.integration.** { *; } +-keep class io.netty.util.internal.Hidden$NettyBlockHoundIntegration { *; } + +# Additional Netty keep rules for R8 +-dontwarn io.netty.** +-keep class io.netty.** { *; } +-keepclassmembers class io.netty.** { *; } + +# BlockHound related classes +-dontwarn reactor.blockhound.** +-keep class reactor.blockhound.** { *; } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TeacherHiltTestApplication.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TeacherHiltTestApplication.kt similarity index 94% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TeacherHiltTestApplication.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TeacherHiltTestApplication.kt index 6a46e72ab0..78268ee0e9 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TeacherHiltTestApplication.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TeacherHiltTestApplication.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.teacher.ui.espresso +package com.instructure.teacher.espresso import dagger.hilt.android.testing.CustomTestApplication diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TeacherHiltTestRunner.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TeacherHiltTestRunner.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TeacherHiltTestRunner.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TeacherHiltTestRunner.kt index e24890c8e6..00f1d3b0f0 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TeacherHiltTestRunner.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TeacherHiltTestRunner.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.teacher.ui.espresso +package com.instructure.teacher.espresso import android.app.Application import android.content.Context diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TestAppManager.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TestAppManager.kt similarity index 88% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TestAppManager.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TestAppManager.kt index 9193324451..fb528a505e 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/espresso/TestAppManager.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/espresso/TestAppManager.kt @@ -14,8 +14,9 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.espresso +package com.instructure.teacher.espresso +import androidx.work.DefaultWorkerFactory import androidx.work.WorkerFactory import com.instructure.pandautils.features.reminder.AlarmScheduler import com.instructure.teacher.utils.BaseAppManager @@ -25,7 +26,7 @@ open class TestAppManager : BaseAppManager() { var workerFactory: WorkerFactory? = null override fun getWorkManagerFactory(): WorkerFactory { - return workerFactory ?: WorkerFactory.getDefaultWorkerFactory() + return workerFactory ?: DefaultWorkerFactory } override fun getScheduler(): AlarmScheduler? = null diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentSubmissionListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentSubmissionListPageTest.kt deleted file mode 100644 index d739f4d407..0000000000 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentSubmissionListPageTest.kt +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (C) 2017 - present Instructure, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init -import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule -import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager -import com.instructure.canvasapi2.models.Assignment -import com.instructure.canvasapi2.models.CanvasContextPermission -import com.instructure.dataseeding.util.ago -import com.instructure.dataseeding.util.days -import com.instructure.dataseeding.util.iso8601 -import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin -import dagger.hilt.android.testing.BindValue -import dagger.hilt.android.testing.HiltAndroidTest -import dagger.hilt.android.testing.UninstallModules -import org.junit.Test - -@HiltAndroidTest -@UninstallModules(CustomGradeStatusModule::class) -class AssignmentSubmissionListPageTest : TeacherComposeTest() { - - @BindValue - @JvmField - val customGradeStatusesManager: CustomGradeStatusesManager = FakeCustomGradeStatusesManager() - - @Test - override fun displaysPageObjects() { - goToAssignmentSubmissionListPage() - assignmentSubmissionListPage.assertPageObjects() - } - - @Test - fun displaysNoSubmissionsView() { - goToAssignmentSubmissionListPage( - students = 0, - submissions = 0 - ) - assignmentSubmissionListPage.assertEmptyViewDisplayed() - } - - @Test - fun filterLateSubmissions() { - goToAssignmentSubmissionListPage( - dueAt = 7.days.ago.iso8601 - ) - assignmentSubmissionListPage.clickFilterButton() - assignmentSubmissionListPage.clickFilterSubmittedLate() - assignmentSubmissionListPage.clickFilterDialogDone() - assignmentSubmissionListPage.assertFilterLabelText("Submitted Late") - assignmentSubmissionListPage.assertHasSubmission() - } - - @Test - fun filterUngradedSubmissions() { - goToAssignmentSubmissionListPage() - assignmentSubmissionListPage.clickFilterButton() - assignmentSubmissionListPage.clickFilterUngraded() - assignmentSubmissionListPage.clickFilterDialogDone() - assignmentSubmissionListPage.assertFilterLabelText("Haven't Been Graded") - assignmentSubmissionListPage.assertHasSubmission() - } - - @Test - fun displaysAssignmentStatusSubmitted() { - goToAssignmentSubmissionListPage() - assignmentSubmissionListPage.assertSubmissionStatusSubmitted() - } - - @Test - fun displaysAssignmentStatusNotSubmitted() { - goToAssignmentSubmissionListPage( - students = 1, - submissions = 0 - ) - assignmentSubmissionListPage.assertSubmissionStatusNotSubmitted() - } - - @Test - fun displaysAssignmentStatusLate() { - goToAssignmentSubmissionListPage( - dueAt = 7.days.ago.iso8601 - ) - assignmentSubmissionListPage.assertSubmissionStatusLate() - } - - @Test - fun messageStudentsWho() { - val data = goToAssignmentSubmissionListPage( - students = 1 - ) - val student = data.students[0] - assignmentSubmissionListPage.clickAddMessage() - - inboxComposePage.assertRecipientSelected(student.shortName!!) - } - - private fun goToAssignmentSubmissionListPage( - students: Int = 1, - submissions: Int = 1, - dueAt: String? = null - ): MockCanvas { - val data = MockCanvas.init(teacherCount = 1, studentCount = students, courseCount = 1, favoriteCourseCount = 1) - val course = data.courses.values.first() - val teacher = data.teachers[0] - - // TODO: Make this part of MockCanvas.init() - data.addCoursePermissions( - course.id, - CanvasContextPermission() // Just need to have some sort of permissions object registered - ) - - val assignment = data.addAssignment( - courseId = course.id, - submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY), - dueAt = dueAt - ) - - for (s in 0 until submissions) { - if(students == 0) { - throw Exception("Can't specify submissions without students") - } - data.addSubmissionForAssignment( - assignmentId = assignment.id, - userId = data.students[0].id, - type = "online_text_entry", - body = "A submission" - ) - } - - val token = data.tokenFor(teacher)!! - tokenLogin(data.domain, token, teacher) - dashboardPage.openCourse(course) - courseBrowserPage.openAssignmentsTab() - assignmentListPage.clickAssignment(assignment) - assignmentDetailsPage.clickAllSubmissions() - - return data - } -} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginFindSchoolPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginFindSchoolPageTest.kt deleted file mode 100644 index 48bdaf59c6..0000000000 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginFindSchoolPageTest.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.instructure.teacher.ui - -import com.instructure.teacher.ui.utils.TeacherTest -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test - -@HiltAndroidTest -class LoginFindSchoolPageTest: TeacherTest() { - - @Test - override fun displaysPageObjects() { - loginLandingPage.clickFindMySchoolButton() - loginFindSchoolPage.assertPageObjects() - } -} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginLandingPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginLandingPageTest.kt deleted file mode 100644 index 2a01e2422b..0000000000 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginLandingPageTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.instructure.teacher.ui - -import com.instructure.espresso.filters.P1 -import com.instructure.teacher.ui.utils.TeacherTest -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test - -@HiltAndroidTest -class LoginLandingPageTest: TeacherTest() { - - // Runs live; no MockCanvas - @Test - @P1 - override fun displaysPageObjects() { - loginLandingPage.assertPageObjects() - } -} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AnnouncementsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/AnnouncementsE2ETest.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AnnouncementsE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/AnnouncementsE2ETest.kt index 13a2f04bf5..b842888adb 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AnnouncementsE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/AnnouncementsE2ETest.kt @@ -14,17 +14,17 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CommentLibraryE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/CommentLibraryE2ETest.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CommentLibraryE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/CommentLibraryE2ETest.kt index 317d57b0f6..f9f6832391 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CommentLibraryE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/CommentLibraryE2ETest.kt @@ -14,14 +14,14 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.CommentLibraryApi import com.instructure.dataseeding.api.SubmissionsApi @@ -34,8 +34,8 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CourseSettingsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/CourseSettingsE2ETest.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CourseSettingsE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/CourseSettingsE2ETest.kt index 324510d742..74152321e0 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/CourseSettingsE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/CourseSettingsE2ETest.kt @@ -14,19 +14,19 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.refresh import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/DashboardE2ETest.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/DashboardE2ETest.kt index f4e20a5206..671824b440 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/DashboardE2ETest.kt @@ -14,18 +14,18 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/DiscussionsE2ETest.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/DiscussionsE2ETest.kt index 5f23ecf55a..9ef7a23056 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/DiscussionsE2ETest.kt @@ -14,19 +14,19 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.lang.Thread.sleep diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/FilesE2ETest.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/FilesE2ETest.kt index cfbbab3973..49eea85991 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/FilesE2ETest.kt @@ -14,16 +14,17 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.os.Environment import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvasapi2.managers.DiscussionManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.weave.awaitApiResponse @@ -35,11 +36,10 @@ import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.FileUploadType import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.Randomizer -import com.instructure.espresso.ViewUtils import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin -import com.instructure.teacher.ui.utils.uploadTextFile +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin +import com.instructure.teacher.ui.utils.extensions.uploadTextFile import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.io.File @@ -135,7 +135,7 @@ class FilesE2ETest: TeacherComposeTest() { fileListPage.assertItemDisplayed(discussionAttachmentFile.name) Log.d(STEP_TAG, "Navigate back to the Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open '${course.name}' course and navigate to Assignments Page.") dashboardPage.openCourse(course.name) @@ -159,7 +159,7 @@ class FilesE2ETest: TeacherComposeTest() { speedGraderPage.assertCommentAttachmentDisplayedCommon(commentUploadInfo.fileName, student.shortName) */ Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(5) + pressBackButton(5) Log.d(STEP_TAG, "Navigate to 'Files' menu in user left-side menu.") leftSideNavigationDrawerPage.clickFilesMenu() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/HelpMenuE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/HelpMenuE2ETest.kt similarity index 83% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/HelpMenuE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/HelpMenuE2ETest.kt index 7a7831a340..ba2df47f7e 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/HelpMenuE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/HelpMenuE2ETest.kt @@ -14,21 +14,22 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.intent.Intents -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.checkToastText import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -42,8 +43,7 @@ class HelpMenuE2ETest: TeacherTest() { @E2E @Test - @TestMetaData(Priority.NICE_TO_HAVE, FeatureCategory.DASHBOARD, TestCategory.E2E) - @Stub + @TestMetaData(Priority.COMMON, FeatureCategory.LEFT_SIDE_MENU, TestCategory.E2E, SecondaryFeatureCategory.HELP_MENU) fun testHelpMenuE2E() { Log.d(PREPARATION_TAG, "Seeding data.") @@ -63,8 +63,8 @@ class HelpMenuE2ETest: TeacherTest() { Log.d(ASSERTION_TAG, "Assert that all the corresponding Help menu content are displayed.") helpPage.assertHelpMenuContent() - Log.d(STEP_TAG, "Click on 'Report a problem' menu.") - helpPage.verifyReportAProblem("Test Subject", "Test Description") + Log.d(STEP_TAG, "Click on 'Report a Problem' menu.") + helpPage.assertReportProblemDialogDetails("Test Subject", "Test Description") Log.d(ASSERTION_TAG, "Assert that it is possible to write into the input fields and the corresponding buttons are displayed as well.") helpPage.assertReportProblemDialogDisplayed() @@ -89,8 +89,7 @@ class HelpMenuE2ETest: TeacherTest() { @E2E @Test - @TestMetaData(Priority.COMMON, FeatureCategory.DASHBOARD, TestCategory.E2E) - @Stub + @TestMetaData(Priority.BUG_CASE, FeatureCategory.LEFT_SIDE_MENU, TestCategory.E2E, SecondaryFeatureCategory.HELP_MENU) fun testHelpMenuReportProblemE2E() { Log.d(PREPARATION_TAG, "Seeding data.") @@ -110,8 +109,8 @@ class HelpMenuE2ETest: TeacherTest() { Log.d(ASSERTION_TAG, "Assert that all the corresponding Help menu content are displayed.") helpPage.assertHelpMenuContent() - Log.d(STEP_TAG, "Click on 'Report a problem' menu.") - helpPage.verifyReportAProblem("Test Subject", "Test Description") + Log.d(STEP_TAG, "Click on 'Report a Problem' menu.") + helpPage.assertReportProblemDialogDetails("Test Subject", "Test Description") Log.d(ASSERTION_TAG, "Assert that it is possible to write into the input fields and the corresponding buttons are displayed as well.") helpPage.assertReportProblemDialogDisplayed() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/LoginE2ETest.kt similarity index 87% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/LoginE2ETest.kt index 3a31e212d4..aed1c65b2d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/LoginE2ETest.kt @@ -14,25 +14,26 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E +import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.dataseeding.api.SeedApi import com.instructure.dataseeding.api.UserApi import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.CourseApiModel -import com.instructure.espresso.ViewUtils import com.instructure.espresso.withIdlingResourceDisabled import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData +import com.instructure.teacher.ui.utils.extensions.seedData import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -259,6 +260,45 @@ class LoginE2ETest : TeacherTest() { canvasNetworkSignInPage.assertPageObjects() } + @E2E + @Test + @TestMetaData(Priority.NICE_TO_HAVE, FeatureCategory.LOGIN, TestCategory.E2E) + fun testWrongDomainE2E() { + + val wrongDomain = "invalid-domain" + val validDomain = "mobileqa.beta" + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1) + val teacher = data.teachersList[0] + + Log.d(STEP_TAG, "Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG, "Enter non-existent domain: $wrongDomain.instructure.com.") + loginFindSchoolPage.enterDomain(wrongDomain) + + Log.d(STEP_TAG, "Click on 'Next' button on the Toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(ASSERTION_TAG, "Assert that the error page is displayed with 'You typed: $wrongDomain.instructure.com' message and the Canvas dinosaur error image.") + wrongDomainPage.assertPageObjects() + wrongDomainPage.assertYouTypedMessageDisplayed(wrongDomain) + wrongDomainPage.assertErrorPageImageDisplayed() + + Log.d(STEP_TAG, "Navigate back to the LoginFindSchoolPage.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Enter valid domain: $validDomain.instructure.com.") + loginFindSchoolPage.enterDomain(validDomain) + + Log.d(STEP_TAG, "Click on 'Next' button on the Toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(ASSERTION_TAG, "Assert that the Login Page is open.") + loginSignInPage.assertPageObjects() + } + private fun loginWithUser(user: CanvasUserApiModel, lastSchoolSaved: Boolean = false) { if(lastSchoolSaved) { @@ -299,7 +339,7 @@ class LoginE2ETest : TeacherTest() { peopleListPage.assertPersonListed(user, role) Log.d(STEP_TAG, "Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) } private fun assertSuccessfulLogin(user: CanvasUserApiModel) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/ModulesE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/ModulesE2ETest.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/ModulesE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/ModulesE2ETest.kt index 921e14ce55..27fc1f485d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/ModulesE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/ModulesE2ETest.kt @@ -1,12 +1,12 @@ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.dataseeding.api.FileFolderApi @@ -23,10 +23,10 @@ import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.getCustomDateCalendar import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.openOverflowMenu -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin -import com.instructure.teacher.ui.utils.uploadTextFile +import com.instructure.teacher.ui.utils.extensions.openOverflowMenu +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin +import com.instructure.teacher.ui.utils.extensions.uploadTextFile import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PagesE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PagesE2ETest.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PagesE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PagesE2ETest.kt index 131c047baa..529a1b7863 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PagesE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PagesE2ETest.kt @@ -1,18 +1,18 @@ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.api.PagesApi -import com.instructure.teacher.ui.pages.WebViewTextCheck +import com.instructure.teacher.ui.pages.classic.WebViewTextCheck import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PeopleE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PeopleE2ETest.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PeopleE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PeopleE2ETest.kt index 5064daa0a7..170841f22f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PeopleE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PeopleE2ETest.kt @@ -14,29 +14,29 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.dataseeding.api.GroupsApi import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.espresso.ViewUtils -import com.instructure.teacher.ui.pages.PeopleListPage -import com.instructure.teacher.ui.pages.PersonContextPage +import com.instructure.teacher.ui.pages.classic.PeopleListPage +import com.instructure.teacher.ui.pages.classic.PersonContextPage import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.seedAssignmentSubmission -import com.instructure.teacher.ui.utils.seedAssignments -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedAssignmentSubmission +import com.instructure.teacher.ui.utils.extensions.seedAssignments +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.lang.Thread.sleep @@ -187,7 +187,7 @@ class PeopleE2ETest: TeacherComposeTest() { peopleListPage.assertSearchResultCount(5) Log.d(STEP_TAG, "Quit from searching and navigate to People List page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Click on the 'Filter' icon on the top-right corner and select '${group.name}' group as a filter.") peopleListPage.clickOnPeopleFilterMenu() @@ -233,7 +233,7 @@ class PeopleE2ETest: TeacherComposeTest() { peopleListPage.assertSearchResultCount(5) Log.d(STEP_TAG, "Navigate back to Dashboard Page. Click on the Inbox bottom menu.") - ViewUtils.pressBackButton(2) + pressBackButton(2) dashboardPage.openInbox() Log.d(ASSERTION_TAG, "Assert that the Inbox is empty.") diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PushNotificationsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PushNotificationsE2ETest.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PushNotificationsE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PushNotificationsE2ETest.kt index a7acb1eacb..76062900e4 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PushNotificationsE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/PushNotificationsE2ETest.kt @@ -1,11 +1,11 @@ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.teacher.BuildConfig import com.instructure.teacher.ui.utils.TeacherComposeTest import dagger.hilt.android.testing.HiltAndroidTest diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/QuizE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/QuizE2ETest.kt similarity index 93% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/QuizE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/QuizE2ETest.kt index 5944219546..7955ef3364 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/QuizE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/QuizE2ETest.kt @@ -14,24 +14,24 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.seedQuizQuestion -import com.instructure.teacher.ui.utils.seedQuizSubmission -import com.instructure.teacher.ui.utils.seedQuizzes -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.seedQuizQuestion +import com.instructure.teacher.ui.utils.extensions.seedQuizSubmission +import com.instructure.teacher.ui.utils.extensions.seedQuizzes +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SettingsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/SettingsE2ETest.kt similarity index 77% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SettingsE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/SettingsE2ETest.kt index a09ec7dcee..196408a926 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SettingsE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/SettingsE2ETest.kt @@ -14,33 +14,33 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.NoMatchingViewException -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.checkToastText +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvasapi2.utils.RemoteConfigParam import com.instructure.canvasapi2.utils.RemoteConfigUtils import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.EnrollmentsApi import com.instructure.dataseeding.util.CanvasNetworkAdapter -import com.instructure.espresso.ViewUtils import com.instructure.pandautils.utils.AppTheme import com.instructure.teacher.BuildConfig import com.instructure.teacher.R -import com.instructure.teacher.ui.pages.PersonContextPage +import com.instructure.teacher.ui.pages.classic.PersonContextPage import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.openLeftSideMenu -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.openLeftSideMenu +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -101,7 +101,7 @@ class SettingsE2ETest : TeacherComposeTest() { profileSettingsPage.assertPronouns(testPronoun) Log.d(STEP_TAG, "Navigate back to the Dashboard Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Select '${course.name}' course and open 'People' tab.") dashboardPage.selectCourse(course) @@ -124,7 +124,7 @@ class SettingsE2ETest : TeacherComposeTest() { CoursesApi.concludeCourse(course.id) // Need to conclude the course because otherwise there would be too much course with time on the dedicated user's dashboard. Log.d(STEP_TAG, "Navigate back to Dashboard.") - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Open the Left Side Menu.") dashboardPage.openLeftSideMenu() @@ -482,7 +482,7 @@ class SettingsE2ETest : TeacherComposeTest() { inboxSignatureSettingsPage.assertSignatureEnabledState(true) Log.d(STEP_TAG, "Navigate back to the Dashboard.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open Inbox Page.") dashboardPage.openInbox() @@ -649,4 +649,177 @@ class SettingsE2ETest : TeacherComposeTest() { Log.d(ASSERTION_TAG, "Assert that the previously set inbox signature text is displayed by default since we logged back with '${teacher.name}' teacher.") inboxComposePage.assertBodyText("\n\n---\nPresident of AC Milan\nVice President of Ferencvaros") } + + @E2E + @Test + @TestMetaData(Priority.BUG_CASE, FeatureCategory.INBOX, TestCategory.E2E, SecondaryFeatureCategory.INBOX_SIGNATURE) + fun testInboxSignaturesFABButtonWithDifferentUsersE2E() { + + //Bug Ticket: MBL-18929 + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(teachers = 2, students = 1, courses = 1) + val teacher = data.teachersList[0] + val teacher2 = data.teachersList[1] + val student = data.studentsList[0] + val course = data.coursesList[0] + + Log.d(STEP_TAG, "Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG, "Enter domain: '${CanvasNetworkAdapter.canvasDomain}'") + loginFindSchoolPage.enterDomain(CanvasNetworkAdapter.canvasDomain) + + Log.d(STEP_TAG, "Click on 'Next' button on the toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(STEP_TAG, "Login with user: '${teacher.name}', login id: '${teacher.loginId}'.") + loginSignInPage.loginAs(teacher) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open the Left Side Navigation Drawer menu.") + dashboardPage.openLeftSideMenu() + + Log.d(STEP_TAG, "Navigate to Settings Page on the left-side menu.") + leftSideNavigationDrawerPage.clickSettingsMenu() + + Log.d(ASSERTION_TAG, "Assert that by default the Inbox Signature is 'Not Set'.") + settingsPage.assertSettingsItemDisplayed("Inbox Signature", "Not Set") + + Log.d(STEP_TAG, "Click on the 'Inbox Signature' settings.") + settingsPage.clickOnSettingsItem("Inbox Signature") + + Log.d(ASSERTION_TAG, "Assert that by default the 'Inbox Signature' toggle is turned off.") + inboxSignatureSettingsPage.assertSignatureEnabledState(false) + + val signatureText = "Best Regards\nCanvas Teacher" + Log.d(STEP_TAG, "Turn on the 'Inbox Signature' and set the inbox signature text to: '$signatureText'. Save the changes.") + inboxSignatureSettingsPage.toggleSignatureEnabledState() + inboxSignatureSettingsPage.changeSignatureText(signatureText) + inboxSignatureSettingsPage.saveChanges() + + Log.d(ASSERTION_TAG, "Assert that the 'Inbox settings saved!' toast message is displayed.") + checkToastText(R.string.inboxSignatureSettingsUpdated, activityRule.activity) + + Log.d(STEP_TAG, "Refresh the Settings page.") + settingsPage.refresh() + + Log.d(ASSERTION_TAG, "Assert that the Inbox Signature became 'Enabled'.") + settingsPage.assertSettingsItemDisplayed("Inbox Signature", "Enabled") + + Log.d(STEP_TAG, "Navigate back to the Dashboard.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Select course: '${course.name}'.") + dashboardPage.selectCourse(course) + + Log.d(STEP_TAG, "Navigate to People page.") + courseBrowserPage.openPeopleTab() + + Log.d(STEP_TAG, "Click on student: '${student.name}' to open Student Context Page.") + peopleListPage.clickPerson(student) + + Log.d(ASSERTION_TAG, "Assert that Student Context Page displays correct student.") + studentContextPage.assertDisplaysStudentInfo(student.shortName, student.loginId) + + Log.d(STEP_TAG, "Click on the 'Compose' button on Student Context Page.") + studentContextPage.clickOnNewMessageButton() + + Log.d(ASSERTION_TAG, "Assert that the inbox signature: '${signatureText}' text is displayed when composing message from Student Context page FAB.") + inboxComposePage.assertBodyText("\n\n---\nBest Regards\nCanvas Teacher") + + Log.d(STEP_TAG, "Click on the 'Close' (X) button on the Compose New Message Page.") + inboxComposePage.clickOnCloseButton() + + Log.d(STEP_TAG, "Navigate back to Dashboard.") + pressBackButton(3) + + Log.d(STEP_TAG, "Click on 'Change User' button on the Left Side Navigation Drawer menu.") + leftSideNavigationDrawerPage.clickChangeUserMenu() + + Log.d(STEP_TAG, "Click on the 'Find another school' button.") + loginLandingPage.clickFindAnotherSchoolButton() + + Log.d(STEP_TAG, "Enter domain: '${CanvasNetworkAdapter.canvasDomain}'") + loginFindSchoolPage.enterDomain(CanvasNetworkAdapter.canvasDomain) + + Log.d(STEP_TAG, "Click on 'Next' button on the toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(STEP_TAG, "Login with the other user: '${teacher2.name}', login id: '${teacher2.loginId}'.") + loginSignInPage.loginAs(teacher2) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Select course: '${course.name}'.") + dashboardPage.selectCourse(course) + + Log.d(STEP_TAG, "Navigate to People page.") + courseBrowserPage.openPeopleTab() + + Log.d(STEP_TAG, "Click on student: '${student.name}' to open Student Context Page.") + peopleListPage.clickPerson(student) + + Log.d(ASSERTION_TAG, "Assert that Student Context Page displays correct student.") + studentContextPage.assertDisplaysStudentInfo(student.shortName, student.loginId) + + Log.d(STEP_TAG, "Click on the 'Compose' button on Student Context Page.") + studentContextPage.clickOnNewMessageButton() + + Log.d(ASSERTION_TAG, "Assert that the previously set inbox signature text is NOT displayed since it was set for another user.") + inboxComposePage.assertBodyText("") + + Log.d(STEP_TAG, "Click on the 'Close' (X) button on the Compose New Message Page.") + inboxComposePage.clickOnCloseButton() + + Log.d(STEP_TAG, "Navigate back to Dashboard.") + pressBackButton(3) + + Log.d(STEP_TAG, "Open the Left Side Navigation Drawer menu.") + dashboardPage.openLeftSideMenu() + + Log.d(STEP_TAG, "Navigate to Settings Page on the Left Side Navigation Drawer menu.") + leftSideNavigationDrawerPage.clickSettingsMenu() + + Log.d(ASSERTION_TAG, "Assert that by default the Inbox Signature is 'Not Set'.") + settingsPage.assertSettingsItemDisplayed("Inbox Signature", "Not Set") + + Log.d(STEP_TAG, "Click on the 'Inbox Signature' settings.") + settingsPage.clickOnSettingsItem("Inbox Signature") + + Log.d(ASSERTION_TAG, "Assert that by default the 'Inbox Signature' toggle is turned off.") + inboxSignatureSettingsPage.assertSignatureEnabledState(false) + + val secondSignatureText = "Loyal member of Instructure" + Log.d(STEP_TAG, "Turn on the 'Inbox Signature' and set the inbox signature text to: '$secondSignatureText'. Save the changes.") + inboxSignatureSettingsPage.toggleSignatureEnabledState() + inboxSignatureSettingsPage.changeSignatureText(secondSignatureText) + inboxSignatureSettingsPage.saveChanges() + + Log.d(STEP_TAG, "Refresh the Settings page.") + settingsPage.refresh() + + Log.d(ASSERTION_TAG, "Assert that the Inbox Signature became 'Enabled'.") + settingsPage.assertSettingsItemDisplayed("Inbox Signature", "Enabled") + + Log.d(STEP_TAG, "Navigate back to the Dashboard.") + Espresso.pressBack() + + Log.d(STEP_TAG, "Select course: '${course.name}'.") + dashboardPage.selectCourse(course) + + Log.d(STEP_TAG, "Navigate to People page.") + courseBrowserPage.openPeopleTab() + + Log.d(STEP_TAG, "Click on student: '${student.name}' to open Student Context Page.") + peopleListPage.clickPerson(student) + + Log.d(ASSERTION_TAG, "Assert that Student Context Page displays correct student.") + studentContextPage.assertDisplaysStudentInfo(student.shortName, student.loginId) + + Log.d(STEP_TAG, "Click on the 'Compose' button on Student Context Page.") + studentContextPage.clickOnNewMessageButton() + + Log.d(ASSERTION_TAG, "Assert that the previously set inbox signature: '$secondSignatureText' text is displayed when composing message from People Details page FAB.") + inboxComposePage.assertBodyText("\n\n---\nLoyal member of Instructure") + } + } \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SyllabusE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/SyllabusE2ETest.kt similarity index 92% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SyllabusE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/SyllabusE2ETest.kt index 267d08a824..935eceb8df 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SyllabusE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/SyllabusE2ETest.kt @@ -1,7 +1,7 @@ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory @@ -11,10 +11,10 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedAssignments -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.seedQuizzes -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedAssignments +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.seedQuizzes +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/TodoE2ETest.kt similarity index 91% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/TodoE2ETest.kt index 8a6d6d214c..6c918e2bc8 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/classic/TodoE2ETest.kt @@ -14,10 +14,10 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.classic import android.util.Log -import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory @@ -27,15 +27,15 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.espresso.ViewUtils +import com.instructure.canvas.espresso.pressBackButton import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.seedAssignmentSubmission -import com.instructure.teacher.ui.utils.seedAssignments -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.seedQuizQuestion -import com.instructure.teacher.ui.utils.seedQuizSubmission -import com.instructure.teacher.ui.utils.seedQuizzes -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedAssignmentSubmission +import com.instructure.teacher.ui.utils.extensions.seedAssignments +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.seedQuizQuestion +import com.instructure.teacher.ui.utils.extensions.seedQuizSubmission +import com.instructure.teacher.ui.utils.extensions.seedQuizzes +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -110,7 +110,7 @@ class TodoE2ETest : TeacherTest() { seedQuizSubmission(courseId = course.id, quizId = testQuiz.id, studentToken = student.token) Log.d(STEP_TAG, "Navigate back to the Dashboard Page.") - ViewUtils.pressBackButton(3) + pressBackButton(3) Log.d(STEP_TAG, "Navigate to 'To Do' Page.") dashboardPage.openTodo() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AssignmentE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/AssignmentE2ETest.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AssignmentE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/AssignmentE2ETest.kt index 2333961c33..c3a07b0aa7 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AssignmentE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/AssignmentE2ETest.kt @@ -14,19 +14,19 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.compose import android.os.SystemClock.sleep import android.util.Log import androidx.test.espresso.Espresso import androidx.test.rule.GrantPermissionRule -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.SectionsApi @@ -39,11 +39,11 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.seedAssignmentSubmission -import com.instructure.teacher.ui.utils.seedAssignments -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin -import com.instructure.teacher.ui.utils.uploadTextFile +import com.instructure.teacher.ui.utils.extensions.seedAssignmentSubmission +import com.instructure.teacher.ui.utils.extensions.seedAssignments +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin +import com.instructure.teacher.ui.utils.extensions.uploadTextFile import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Rule import org.junit.Test @@ -133,9 +133,6 @@ class AssignmentE2ETest : TeacherComposeTest() { assignmentSubmissionListPage.filterBySection(course.name) assignmentSubmissionListPage.clickFilterDialogDone() - Log.d(ASSERTION_TAG, "Assert that the 'Clear filter' button is displayed as we set some filter. Assert that the filter label text is the 'All Submissions' text plus the '${course.name}' course name.") - assignmentSubmissionListPage.assertFilterLabelText("All Submissions") - Log.d(STEP_TAG, "Open '${student.name}' student's submission.") assignmentSubmissionListPage.clickSubmission(student) @@ -154,9 +151,6 @@ class AssignmentE2ETest : TeacherComposeTest() { assignmentSubmissionListPage.filterBySection(course.name) assignmentSubmissionListPage.clickFilterDialogDone() - Log.d(ASSERTION_TAG, "Assert that the 'Clear filter' button is NOT displayed as we just cleared the filter. Assert that the filter label text 'All Submission'.") - assignmentSubmissionListPage.assertFilterLabelText("All Submissions") - Log.d(STEP_TAG, "Navigate back to Assignment List Page, open the '${assignment[0].name}' assignment and publish it. Click on Save.") Espresso.pressBack() composeTestRule.waitForIdle() @@ -627,9 +621,6 @@ class AssignmentE2ETest : TeacherComposeTest() { assignmentSubmissionListPage.filterBySection(secondSection.name) assignmentSubmissionListPage.clickFilterDialogDone() - Log.d(ASSERTION_TAG, "Assert that the filter label text is the 'All Submissions'.") - assignmentSubmissionListPage.assertFilterLabelText("All Submissions") // I'm not sure if this is right to show this?! Shouldn't we someone display that there is a filter applied for a particular section? - Log.d(ASSERTION_TAG, "Assert that there is 1 submission displayed, and it is for '${student2.name}' student since we applied a filter to the '${secondSection.name}' section which the '${student2.name}' student is in, and the submission of the '${student.name}' student is filtered out because it's not in the '${secondSection.name}' section.") assignmentSubmissionListPage.assertHasSubmission(1) assignmentSubmissionListPage.assertHasStudentSubmission(student2) @@ -643,9 +634,6 @@ class AssignmentE2ETest : TeacherComposeTest() { assignmentSubmissionListPage.filterBySection(firstSection.name) assignmentSubmissionListPage.clickFilterDialogDone() - Log.d(ASSERTION_TAG, "Assert that the filter label text is the 'All Submissions'.") - assignmentSubmissionListPage.assertFilterLabelText("All Submissions") // I'm not sure if this is right to show this?! Shouldn't we someone display that there is a filter applied for a particular section? - Log.d(ASSERTION_TAG, "Assert that there is 1 submission displayed, and it is for '${student.name}' student since we applied a filter to the '${firstSection.name}' section which the '${student.name}' student is in, and the submission of the '${student2.name}' student is filtered out because it's not in the '${firstSection.name}' section.") assignmentSubmissionListPage.assertHasSubmission(1) assignmentSubmissionListPage.assertHasStudentSubmission(student) @@ -658,9 +646,6 @@ class AssignmentE2ETest : TeacherComposeTest() { assignmentSubmissionListPage.filterBySection(firstSection.name) assignmentSubmissionListPage.clickFilterDialogDone() - Log.d(ASSERTION_TAG, "Assert that the filter label text is the 'All Submissions'.") - assignmentSubmissionListPage.assertFilterLabelText("All Submissions") - Log.d(ASSERTION_TAG, "Assert that there are 2 submissions displayed, and they are for '${student.name}' and '${student2.name}' students since we does not apply any section filter yet.") assignmentSubmissionListPage.assertHasSubmission(2) assignmentSubmissionListPage.assertHasStudentSubmission(student) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/CalendarE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/CalendarE2ETest.kt index 2c70758a65..dd77cfa54a 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/CalendarE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/CalendarE2ETest.kt @@ -16,17 +16,17 @@ package com.instructure.teacher.ui.e2e.compose import android.util.Log -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E import com.instructure.espresso.getDateInCanvasCalendarFormat import com.instructure.pandautils.features.calendar.CalendarPrefs import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.clickCalendarTab -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.clickCalendarTab +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/InboxE2ETest.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/InboxE2ETest.kt index 1436c9cdb1..9b4e3d99f5 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/InboxE2ETest.kt @@ -1,14 +1,14 @@ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.compose import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.ReleaseExclude import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.annotations.ReleaseExclude import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.GroupsApi @@ -17,8 +17,8 @@ import com.instructure.dataseeding.model.CourseApiModel import com.instructure.espresso.retry import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/SpeedGraderE2ETest.kt similarity index 88% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/SpeedGraderE2ETest.kt index f490ada8d6..97daf5ee4b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/compose/SpeedGraderE2ETest.kt @@ -14,32 +14,32 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.e2e +package com.instructure.teacher.ui.e2e.compose import android.util.Log -import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.printToLog +import androidx.compose.ui.test.ExperimentalTestApi import androidx.test.espresso.Espresso -import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.annotations.E2E +import com.instructure.canvas.espresso.pressBackButton import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.espresso.ViewUtils import com.instructure.espresso.retry +import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.teacher.R -import com.instructure.teacher.ui.pages.PersonContextPage +import com.instructure.teacher.ui.pages.classic.PersonContextPage import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.seedAssignmentSubmission -import com.instructure.teacher.ui.utils.seedAssignments -import com.instructure.teacher.ui.utils.seedData -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.seedAssignmentSubmission +import com.instructure.teacher.ui.utils.extensions.seedAssignments +import com.instructure.teacher.ui.utils.extensions.seedData +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -50,6 +50,7 @@ class SpeedGraderE2ETest : TeacherComposeTest() { override fun enableAndConfigureAccessibilityChecks() = Unit + @OptIn(ExperimentalTestApi::class) @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.GRADES, TestCategory.E2E) @@ -120,11 +121,10 @@ class SpeedGraderE2ETest : TeacherComposeTest() { Log.d(STEP_TAG, "Open 'Not Submitted' submissions.") assignmentDetailsPage.clickNotSubmittedSubmissions() - Log.d(ASSERTION_TAG, "Assert that the 'Haven't Submitted Yet' label is displayed (as we filtered for only 'Not Submitted') and the submission of '${noSubStudent.name}' student is displayed.") - assignmentSubmissionListPage.assertFilterLabelNotSubmittedSubmissions() - assignmentSubmissionListPage.assertHasStudentSubmission(noSubStudent) - - composeTestRule.onRoot().printToLog("SEMANTIC_TREE") + Log.d(ASSERTION_TAG, "Assert that the submission of '${noSubStudent.name}' student is displayed.") + retryWithIncreasingDelay(initialDelay = 500, maxDelay = 5000, times = 5) { + assignmentSubmissionListPage.assertHasStudentSubmission(noSubStudent) + } Log.d(ASSERTION_TAG, "Assert that the '${noSubStudent.name}' student has '-' as score as it's submission is not submitted yet.") assignmentSubmissionListPage.assertStudentScoreText(noSubStudent.name, "-") @@ -146,7 +146,7 @@ class SpeedGraderE2ETest : TeacherComposeTest() { studentContextPage.assertSectionNameView(PersonContextPage.UserRole.STUDENT) Log.d(STEP_TAG, "Navigate back to the Assignment Details Page.") - ViewUtils.pressBackButton(2) + pressBackButton(2) Log.d(STEP_TAG, "Open 'Graded' submissions.") assignmentDetailsPage.clickGradedSubmissions() @@ -180,20 +180,17 @@ class SpeedGraderE2ETest : TeacherComposeTest() { Espresso.pressBack() assignmentSubmissionListPage.refresh() - Log.d(ASSERTION_TAG, "Assert that the 'All Submissions' filter text is displayed.") - assignmentSubmissionListPage.assertFilterLabelAllSubmissions() - Log.d(STEP_TAG, "Click on filter button and click on 'Filter submissions'.") assignmentSubmissionListPage.clickFilterButton() Log.d(ASSERTION_TAG, "Assert that all the corresponding filter texts are displayed.") - assignmentSubmissionListPage.assertSubmissionFilterOption("All Submissions") - assignmentSubmissionListPage.assertSubmissionFilterOption("Submitted Late") + assignmentSubmissionListPage.assertSubmissionFilterOption("Late") + assignmentSubmissionListPage.assertSubmissionFilterOption("Missing") assignmentSubmissionListPage.assertSubmissionFilterOption("Needs Grading") - assignmentSubmissionListPage.assertSubmissionFilterOption("Not Submitted") + assignmentSubmissionListPage.assertSubmissionFilterOption("Submitted") assignmentSubmissionListPage.assertSubmissionFilterOption("Graded") - assignmentSubmissionListPage.assertSubmissionFilterOption("Scored Less Thanâ€Ļ") - assignmentSubmissionListPage.assertSubmissionFilterOption("Scored More Thanâ€Ļ") + assignmentSubmissionListPage.assertPreciseFilterOption("Scored Less Thanâ€Ļ") + assignmentSubmissionListPage.assertPreciseFilterOption("Scored More Thanâ€Ļ") Log.d(STEP_TAG, "Select 'Not Graded' and click on 'OK'.") assignmentSubmissionListPage.clickFilterUngraded() @@ -252,8 +249,11 @@ class SpeedGraderE2ETest : TeacherComposeTest() { postSettingsPage.clickOnHideGradesButton() Log.d(ASSERTION_TAG, "Assert that the hide grades (eye) icon is displayed next to the corresponding (graded) students.") - assignmentSubmissionListPage.assertGradesHidden(gradedStudent.name) - assignmentSubmissionListPage.assertGradesHidden(student.name) + retryWithIncreasingDelay(initialDelay = 500, maxDelay = 5000, times = 5) { + assignmentSubmissionListPage.assertGradesHidden(gradedStudent.name) + assignmentSubmissionListPage.assertGradesHidden(student.name) + } + } } \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AnnouncementsListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AnnouncementsListInteractionTest.kt similarity index 90% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AnnouncementsListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AnnouncementsListInteractionTest.kt index 0c2ef77360..b5ab630e5b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AnnouncementsListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AnnouncementsListInteractionTest.kt @@ -14,27 +14,27 @@ * limitations under the License. * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import androidx.test.espresso.matcher.ViewMatchers.withId import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesCheckNames import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesViews -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Tab import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.`is` import org.junit.Test @HiltAndroidTest -class AnnouncementsListPageTest : TeacherTest() { +class AnnouncementsListInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssigneeListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssigneeListInteractionTest.kt similarity index 89% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssigneeListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssigneeListInteractionTest.kt index c1281f126b..e0dda5d529 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssigneeListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssigneeListInteractionTest.kt @@ -13,24 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.espresso.assertContainsText import com.instructure.espresso.page.onViewWithId import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class AssigneeListPageTest : TeacherComposeTest() { +class AssigneeListInteractionTest : TeacherComposeTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDetailsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentDetailsInteractionTest.kt similarity index 92% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDetailsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentDetailsInteractionTest.kt index b4a4135739..8978ddb7b7 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDetailsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentDetailsInteractionTest.kt @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.init -import com.instructure.canvas.espresso.mockCanvas.utils.Randomizer +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvas.espresso.mockcanvas.utils.Randomizer import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Assignment.SubmissionType import com.instructure.canvasapi2.models.Assignment.SubmissionType.ONLINE_TEXT_ENTRY @@ -34,12 +34,12 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class AssignmentDetailsPageTest : TeacherComposeTest() { +class AssignmentDetailsInteractionTest : TeacherComposeTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDueDatesPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentDueDatesInteractionTest.kt similarity index 87% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDueDatesPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentDueDatesInteractionTest.kt index f01494909a..aa844bc695 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AssignmentDueDatesPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentDueDatesInteractionTest.kt @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.dataseeding.util.ago @@ -26,12 +26,12 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class AssignmentDueDatesPageTest : TeacherComposeTest() { +class AssignmentDueDatesInteractionTest : TeacherComposeTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentSubmissionListInteractionTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentSubmissionListInteractionTest.kt new file mode 100644 index 0000000000..7885c9c455 --- /dev/null +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/AssignmentSubmissionListInteractionTest.kt @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2017 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeDifferentiationTagsManager +import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvasapi2.DifferentiationTagsQuery +import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule +import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager +import com.instructure.canvasapi2.managers.graphql.DifferentiationTagsManager +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.pandautils.di.DifferentiationTagsModule +import com.instructure.dataseeding.util.ago +import com.instructure.dataseeding.util.days +import com.instructure.dataseeding.util.iso8601 +import com.instructure.teacher.ui.utils.TeacherComposeTest +import com.instructure.teacher.ui.utils.extensions.tokenLogin +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules +import org.junit.Test + +@HiltAndroidTest +@UninstallModules(CustomGradeStatusModule::class, DifferentiationTagsModule::class) +class AssignmentSubmissionListInteractionTest : TeacherComposeTest() { + + @BindValue + @JvmField + val customGradeStatusesManager: CustomGradeStatusesManager = FakeCustomGradeStatusesManager() + + // Default to no differentiation tags - tests that need tags will override this + @BindValue + @JvmField + var differentiationTagsManager: DifferentiationTagsManager = FakeDifferentiationTagsManager() + + @Test + override fun displaysPageObjects() { + goToAssignmentSubmissionListPage() + assignmentSubmissionListPage.assertPageObjects() + } + + @Test + fun displaysNoSubmissionsView() { + goToAssignmentSubmissionListPage( + students = 0, + submissions = 0 + ) + assignmentSubmissionListPage.assertEmptyViewDisplayed() + } + + @Test + fun filterLateSubmissions() { + goToAssignmentSubmissionListPage( + dueAt = 7.days.ago.iso8601 + ) + assignmentSubmissionListPage.clickFilterButton() + assignmentSubmissionListPage.clickFilterSubmittedLate() + assignmentSubmissionListPage.clickFilterDialogDone() + assignmentSubmissionListPage.assertHasSubmission() + } + + @Test + fun filterUngradedSubmissions() { + goToAssignmentSubmissionListPage() + assignmentSubmissionListPage.clickFilterButton() + assignmentSubmissionListPage.clickFilterUngraded() + assignmentSubmissionListPage.clickFilterDialogDone() + assignmentSubmissionListPage.assertHasSubmission() + } + + @Test + fun displaysAssignmentStatusSubmitted() { + goToAssignmentSubmissionListPage() + assignmentSubmissionListPage.assertSubmissionStatusSubmitted() + } + + @Test + fun displaysAssignmentStatusNotSubmitted() { + goToAssignmentSubmissionListPage( + students = 1, + submissions = 0 + ) + assignmentSubmissionListPage.assertSubmissionStatusNotSubmitted() + } + + @Test + fun displaysAssignmentStatusLate() { + goToAssignmentSubmissionListPage( + dueAt = 7.days.ago.iso8601 + ) + assignmentSubmissionListPage.assertSubmissionStatusLate() + } + + @Test + fun messageStudentsWho() { + val data = goToAssignmentSubmissionListPage( + students = 1 + ) + val student = data.students[0] + assignmentSubmissionListPage.clickAddMessage() + + inboxComposePage.assertRecipientSelected(student.shortName!!) + } + + @Test + fun filterBySection() { + val data = goToAssignmentSubmissionListPage( + students = 2, + submissions = 2, + createSections = true + ) + val section = data.courses.values.first().sections.first() + + assignmentSubmissionListPage.clickFilterButton() + assignmentSubmissionListPage.filterBySection(section.name) + assignmentSubmissionListPage.clickFilterDialogDone() + + // Should show all students since they all belong to the same section + assignmentSubmissionListPage.assertHasSubmission(expectedCount = 2) + } + + @Test + fun filterByCustomGradeStatus() { + goToAssignmentSubmissionListPage() + + assignmentSubmissionListPage.clickFilterButton() + + // Verify custom status options are available (from FakeCustomGradeStatusesManager) + assignmentSubmissionListPage.assertCustomStatusFilterOption("Custom Status 1") + assignmentSubmissionListPage.assertCustomStatusFilterOption("Custom Status 2") + assignmentSubmissionListPage.assertCustomStatusFilterOption("Custom Status 3") + + // Select a custom status filter + assignmentSubmissionListPage.clickFilterCustomStatus("Custom Status 1") + assignmentSubmissionListPage.clickFilterDialogDone() + + // Custom statuses are not assigned by default in MockCanvas, + // so this should show no submissions + assignmentSubmissionListPage.assertHasNoSubmission() + } + + @Test + fun filterMultipleStatusesWithOrLogic() { + goToAssignmentSubmissionListPage( + students = 2, + submissions = 1 + ) + + assignmentSubmissionListPage.clickFilterButton() + + // Select multiple filters - should use OR logic + assignmentSubmissionListPage.clickFilterUngraded() + assignmentSubmissionListPage.clickFilterSubmitted() + assignmentSubmissionListPage.clickFilterDialogDone() + + // Should show submissions matching either ungraded OR submitted + // With 2 students and 1 submission, we have 1 ungraded submission + assignmentSubmissionListPage.assertHasSubmission(expectedCount = 1) + } + + @Test + fun filterIncludeStudentsWithoutDifferentiationTags() { + // Set up differentiation tags for this test + differentiationTagsManager = FakeDifferentiationTagsManager( + listOf( + DifferentiationTagsQuery.Group( + _id = "tag1", + name = "Advanced Learners", + nonCollaborative = true, + membersConnection = null + ) + ) + ) + + goToAssignmentSubmissionListPage() + + assignmentSubmissionListPage.clickFilterButton() + + // Select to include students without tags + assignmentSubmissionListPage.clickIncludeStudentsWithoutTags() + assignmentSubmissionListPage.clickFilterDialogDone() + + // Should show all students since none have tags assigned + assignmentSubmissionListPage.assertHasSubmission(expectedCount = 1) + } + + @Test + fun filterMultipleDifferentiationTagsWithOrLogic() { + // Set up differentiation tags for this test + differentiationTagsManager = FakeDifferentiationTagsManager( + listOf( + DifferentiationTagsQuery.Group( + _id = "tag1", + name = "Advanced Learners", + nonCollaborative = true, + membersConnection = null + ), + DifferentiationTagsQuery.Group( + _id = "tag2", + name = "English Language Learners", + nonCollaborative = true, + membersConnection = null + ), + DifferentiationTagsQuery.Group( + _id = "tag3", + name = "Special Education", + nonCollaborative = true, + membersConnection = null + ) + ) + ) + + goToAssignmentSubmissionListPage( + students = 2, + submissions = 2 + ) + + assignmentSubmissionListPage.clickFilterButton() + + // Select multiple differentiation tag filters - should use OR logic + assignmentSubmissionListPage.clickFilterDifferentiationTag("Advanced Learners") + assignmentSubmissionListPage.clickFilterDifferentiationTag("English Language Learners") + assignmentSubmissionListPage.clickFilterDialogDone() + + // Since no students have tags assigned, should show no submissions + assignmentSubmissionListPage.assertHasNoSubmission() + } + + @Test + fun filterDifferentiationTagsWithIncludeWithoutTags() { + // Set up differentiation tags for this test + differentiationTagsManager = FakeDifferentiationTagsManager( + listOf( + DifferentiationTagsQuery.Group( + _id = "tag1", + name = "Advanced Learners", + nonCollaborative = true, + membersConnection = null + ) + ) + ) + + goToAssignmentSubmissionListPage( + students = 2, + submissions = 2 + ) + + assignmentSubmissionListPage.clickFilterButton() + + // Select a tag AND include students without tags + assignmentSubmissionListPage.clickFilterDifferentiationTag("Advanced Learners") + assignmentSubmissionListPage.clickIncludeStudentsWithoutTags() + assignmentSubmissionListPage.clickFilterDialogDone() + + // Should show all students since they don't have tags (included by the checkbox) + assignmentSubmissionListPage.assertHasSubmission(expectedCount = 2) + } + + private fun goToAssignmentSubmissionListPage( + students: Int = 1, + submissions: Int = 1, + dueAt: String? = null, + createSections: Boolean = false + ): MockCanvas { + val data = MockCanvas.init( + teacherCount = 1, + studentCount = students, + courseCount = 1, + favoriteCourseCount = 1, + createSections = createSections + ) + val course = data.courses.values.first() + val teacher = data.teachers[0] + + // TODO: Make this part of MockCanvas.init() + data.addCoursePermissions( + course.id, + CanvasContextPermission() // Just need to have some sort of permissions object registered + ) + + val assignment = data.addAssignment( + courseId = course.id, + submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY), + dueAt = dueAt + ) + + for (s in 0 until submissions) { + if(students == 0) { + throw Exception("Can't specify submissions without students") + } + data.addSubmissionForAssignment( + assignmentId = assignment.id, + userId = data.students[0].id, + type = "online_text_entry", + body = "A submission" + ) + } + + val token = data.tokenFor(teacher)!! + tokenLogin(data.domain, token, teacher) + dashboardPage.openCourse(course) + courseBrowserPage.openAssignmentsTab() + assignmentListPage.clickAssignment(assignment) + assignmentDetailsPage.clickAllSubmissions() + + return data + } +} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CommentLibraryPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CommentLibraryInteractionTest.kt similarity index 89% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CommentLibraryPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CommentLibraryInteractionTest.kt index f5c7935b80..3a8f7d2957 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CommentLibraryPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CommentLibraryInteractionTest.kt @@ -14,29 +14,30 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority -import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeStudentContextManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeStudentContextManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeDifferentiationTagsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -44,22 +45,24 @@ import com.instructure.canvasapi2.managers.PostPolicyManager import com.instructure.canvasapi2.managers.StudentContextManager import com.instructure.canvasapi2.managers.SubmissionRubricManager import com.instructure.canvasapi2.managers.graphql.AssignmentDetailsManager +import com.instructure.canvasapi2.managers.graphql.DifferentiationTagsManager import com.instructure.canvasapi2.managers.graphql.SubmissionCommentsManager import com.instructure.canvasapi2.managers.graphql.SubmissionContentManager import com.instructure.canvasapi2.managers.graphql.SubmissionDetailsManager import com.instructure.canvasapi2.managers.graphql.SubmissionGradeManager +import com.instructure.pandautils.di.DifferentiationTagsModule import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules import org.junit.Test -@UninstallModules(GraphQlApiModule::class) +@UninstallModules(GraphQlApiModule::class, DifferentiationTagsModule::class) @HiltAndroidTest -class CommentLibraryPageTest : TeacherComposeTest() { +class CommentLibraryInteractionTest : TeacherComposeTest() { override fun displaysPageObjects() = Unit @@ -67,6 +70,10 @@ class CommentLibraryPageTest : TeacherComposeTest() { @JvmField val commentLibraryManager: CommentLibraryManager = FakeCommentLibraryManager() + @BindValue + @JvmField + val differentiationTagsManager: DifferentiationTagsManager = FakeDifferentiationTagsManager() + @BindValue @JvmField val postPolicyManager: PostPolicyManager = FakePostPolicyManager() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseBrowserPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CourseBrowserInteractionTest.kt similarity index 81% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseBrowserPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CourseBrowserInteractionTest.kt index 23fab0637c..ea1f719da8 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseBrowserPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CourseBrowserInteractionTest.kt @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class CourseBrowserPageTest : TeacherTest() { +class CourseBrowserInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseSettingsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CourseSettingsInteractionTest.kt similarity index 87% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseSettingsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CourseSettingsInteractionTest.kt index 293c165756..d653ace168 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/CourseSettingsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/CourseSettingsInteractionTest.kt @@ -13,18 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.espresso.randomString import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class CourseSettingsPageTest : TeacherTest() { +class CourseSettingsInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DashboardPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/DashboardInteractionTest.kt similarity index 90% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DashboardPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/DashboardInteractionTest.kt index ed8c118f6c..c31627f374 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DashboardPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/DashboardInteractionTest.kt @@ -15,17 +15,17 @@ * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class DashboardPageTest : TeacherTest() { +class DashboardInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DiscussionsListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/DiscussionsListInteractionTest.kt similarity index 87% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DiscussionsListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/DiscussionsListInteractionTest.kt index 1f962bc1a0..f5e4c6d1d5 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/DiscussionsListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/DiscussionsListInteractionTest.kt @@ -14,22 +14,22 @@ * limitations under the License. * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Tab import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class DiscussionsListPageTest : TeacherTest() { +class DiscussionsListInteractionTest : TeacherTest() { lateinit var course: Course diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditAssignmentDetailsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditAssignmentDetailsInteractionTest.kt similarity index 94% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditAssignmentDetailsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditAssignmentDetailsInteractionTest.kt index 0db5a0bc92..e738f9fa8f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditAssignmentDetailsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditAssignmentDetailsInteractionTest.kt @@ -14,14 +14,14 @@ * limitations under the License. * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.utils.NumberHelper @@ -29,13 +29,13 @@ import com.instructure.espresso.randomDouble import com.instructure.espresso.randomString import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.junit.Test @HiltAndroidTest -class EditAssignmentDetailsPageTest : TeacherComposeTest() { +class EditAssignmentDetailsInteractionTest : TeacherComposeTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditDashboardPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditDashboardInteractionTest.kt similarity index 89% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditDashboardPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditDashboardInteractionTest.kt index f39d3e15aa..9f0ef1cee4 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditDashboardPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditDashboardInteractionTest.kt @@ -15,17 +15,17 @@ * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class EditDashboardPageTest : TeacherTest() { +class EditDashboardInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditQuizDetailsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditQuizDetailsInteractionTest.kt similarity index 94% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditQuizDetailsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditQuizDetailsInteractionTest.kt index 24014291c9..4b2b241031 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditQuizDetailsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditQuizDetailsInteractionTest.kt @@ -14,26 +14,26 @@ * limitations under the License. * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import androidx.test.espresso.matcher.ViewMatchers import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Quiz import com.instructure.espresso.randomString import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.Matchers import org.junit.Test @HiltAndroidTest -class EditQuizDetailsPageTest : TeacherTest() { +class EditQuizDetailsInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditSyllabusPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditSyllabusInteractionTest.kt similarity index 87% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditSyllabusPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditSyllabusInteractionTest.kt index 464acc9690..8803246ace 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/EditSyllabusPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/EditSyllabusInteractionTest.kt @@ -14,15 +14,15 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import android.os.Build -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addCourseSettings -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCourseCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addCourseSettings +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.CourseSettings @@ -32,12 +32,12 @@ import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.espresso.ActivityHelper import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class EditSyllabusPageTest : TeacherTest() { +class EditSyllabusInteractionTest : TeacherTest() { override fun displaysPageObjects() = Unit diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InAppUpdatePageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/InAppUpdateInteractionTest.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InAppUpdatePageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/InAppUpdateInteractionTest.kt index dcf6a78b7e..7958f4fa22 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InAppUpdatePageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/InAppUpdateInteractionTest.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import android.app.NotificationManager import android.content.Context @@ -23,16 +23,15 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.google.android.play.core.appupdate.testing.FakeAppUpdateManager -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.utils.toApiString import com.instructure.pandautils.di.UpdateModule import com.instructure.pandautils.update.UpdateManager import com.instructure.pandautils.update.UpdatePrefs import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/ModuleListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/ModuleListInteractionTest.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/ModuleListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/ModuleListInteractionTest.kt index 5bcdec9eab..c8096e1a16 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/ModuleListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/ModuleListInteractionTest.kt @@ -16,15 +16,15 @@ * */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addItemToModule -import com.instructure.canvas.espresso.mockCanvas.addModuleToCourse -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addItemToModule +import com.instructure.canvas.espresso.mockcanvas.addModuleToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.ModuleContentDetails @@ -32,13 +32,13 @@ import com.instructure.canvasapi2.models.Tab import com.instructure.dataseeding.util.Randomizer import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.openOverflowMenu -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.openOverflowMenu +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class ModuleListPageTest : TeacherComposeTest() { +class ModuleListInteractionTest : TeacherComposeTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/NavDrawerPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/NavDrawerInteractionTest.kt similarity index 81% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/NavDrawerPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/NavDrawerInteractionTest.kt index 4e05ba7cd3..4003679480 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/NavDrawerPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/NavDrawerInteractionTest.kt @@ -15,19 +15,19 @@ * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.openLeftSideMenu -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.openLeftSideMenu +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class NavDrawerPageTest: TeacherTest() { +class NavDrawerInteractionTest: TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/NotATeacherPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/NotATeacherInteractionTest.kt similarity index 82% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/NotATeacherPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/NotATeacherInteractionTest.kt index fdabaaf522..60e867987a 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/NotATeacherPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/NotATeacherInteractionTest.kt @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.slowLogInAsStudent +import com.instructure.teacher.ui.utils.extensions.slowLogInAsStudent import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class NotATeacherPageTest : TeacherTest() { +class NotATeacherInteractionTest : TeacherTest() { // Runs live; no MockCanvas @Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/PageListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/PageListInteractionTest.kt similarity index 86% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/PageListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/PageListInteractionTest.kt index db39f7c554..77cf1e453b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/PageListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/PageListInteractionTest.kt @@ -13,22 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addPageToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addPageToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Page import com.instructure.canvasapi2.models.Tab import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class PageListPageTest : TeacherTest() { +class PageListInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/PersonContextPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/PersonContextInteractionTest.kt similarity index 83% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/PersonContextPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/PersonContextInteractionTest.kt index 16c01111a8..c72f6c44ee 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/PersonContextPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/PersonContextInteractionTest.kt @@ -13,23 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addSubmissionsForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeStudentContextManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addSubmissionsForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeStudentContextManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -46,9 +46,9 @@ import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.models.User -import com.instructure.teacher.ui.pages.PersonContextPage +import com.instructure.teacher.ui.pages.classic.PersonContextPage import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -56,7 +56,7 @@ import org.junit.Test @UninstallModules(GraphQlApiModule::class) @HiltAndroidTest -class PersonContextPageTest : TeacherTest() { +class PersonContextInteractionTest : TeacherTest() { @BindValue @JvmField diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizDetailsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizDetailsInteractionTest.kt similarity index 89% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizDetailsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizDetailsInteractionTest.kt index d6177861e7..03b35f3204 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizDetailsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizDetailsInteractionTest.kt @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addQuizSubmission -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addQuizSubmission +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Quiz import com.instructure.dataseeding.util.ago @@ -27,12 +27,12 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class QuizDetailsPageTest: TeacherTest() { +class QuizDetailsInteractionTest: TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizListInteractionTest.kt similarity index 87% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizListInteractionTest.kt index 2a0ad9c017..d3d2bacef4 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizListInteractionTest.kt @@ -14,24 +14,24 @@ * limitations under the License. * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Quiz import com.instructure.dataseeding.util.ago import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class QuizListPageTest : TeacherTest() { +class QuizListInteractionTest : TeacherTest() { @Test override fun displaysPageObjects() { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizSubmissionListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizSubmissionListInteractionTest.kt similarity index 83% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizSubmissionListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizSubmissionListInteractionTest.kt index a05f0e8d8a..453cc9eebc 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizSubmissionListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/QuizSubmissionListInteractionTest.kt @@ -14,17 +14,20 @@ * limitations under the License. * */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addQuestionToQuiz -import com.instructure.canvas.espresso.mockCanvas.addQuizSubmission -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addQuestionToQuiz +import com.instructure.canvas.espresso.mockcanvas.addQuizSubmission +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeDifferentiationTagsManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager +import com.instructure.canvasapi2.managers.graphql.DifferentiationTagsManager +import com.instructure.pandautils.di.DifferentiationTagsModule import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Quiz import com.instructure.canvasapi2.models.QuizAnswer @@ -32,20 +35,24 @@ import com.instructure.dataseeding.util.ago import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules import org.junit.Test @HiltAndroidTest -@UninstallModules(CustomGradeStatusModule::class) -class QuizSubmissionListPageTest : TeacherComposeTest() { +@UninstallModules(CustomGradeStatusModule::class, DifferentiationTagsModule::class) +class QuizSubmissionListInteractionTest : TeacherComposeTest() { @BindValue @JvmField val customGradeStatusesManager: CustomGradeStatusesManager = FakeCustomGradeStatusesManager() + @BindValue + @JvmField + val differentiationTagsManager: DifferentiationTagsManager = FakeDifferentiationTagsManager() + @Test override fun displaysPageObjects() { goToAssignmentSubmissionListPage() @@ -70,7 +77,6 @@ class QuizSubmissionListPageTest : TeacherComposeTest() { assignmentSubmissionListPage.clickFilterButton() assignmentSubmissionListPage.clickFilterSubmittedLate() assignmentSubmissionListPage.clickFilterDialogDone() - assignmentSubmissionListPage.assertFilterLabelText("Submitted Late") assignmentSubmissionListPage.assertHasSubmission() } @@ -81,7 +87,6 @@ class QuizSubmissionListPageTest : TeacherComposeTest() { assignmentSubmissionListPage.clickFilterButton() assignmentSubmissionListPage.clickFilterUngraded() assignmentSubmissionListPage.clickFilterDialogDone() - assignmentSubmissionListPage.assertFilterLabelText("Haven't Been Graded") assignmentSubmissionListPage.assertHasSubmission() } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderCommentsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderCommentsInteractionTest.kt similarity index 87% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderCommentsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderCommentsInteractionTest.kt index e8dcf11b66..953cf96d5b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderCommentsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderCommentsInteractionTest.kt @@ -13,27 +13,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addSubmissionsForAssignment -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addSubmissionsForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeDifferentiationTagsManager +import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvasapi2.managers.graphql.DifferentiationTagsManager import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.models.SubmissionComment import com.instructure.espresso.randomString +import com.instructure.pandautils.di.DifferentiationTagsModule import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin +import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules import org.junit.Test @HiltAndroidTest -class SpeedGraderCommentsPageTest : TeacherComposeTest() { +@UninstallModules(DifferentiationTagsModule::class) +class SpeedGraderCommentsInteractionTest : TeacherComposeTest() { + + @BindValue + @JvmField + val differentiationTagsManager: DifferentiationTagsManager = FakeDifferentiationTagsManager() // Just good enough to mock the *representation* of a file, not to mock the file itself. val attachment = Attachment( diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderFilesPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderFilesInteractionTest.kt similarity index 77% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderFilesPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderFilesInteractionTest.kt index 8100a1af12..5c1ff85cf6 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderFilesPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderFilesInteractionTest.kt @@ -13,25 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeStudentContextManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeDifferentiationTagsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeStudentContextManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.CommentLibraryManager @@ -41,15 +42,17 @@ import com.instructure.canvasapi2.managers.StudentContextManager import com.instructure.canvasapi2.managers.SubmissionRubricManager import com.instructure.canvasapi2.managers.graphql.AssignmentDetailsManager import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager +import com.instructure.canvasapi2.managers.graphql.DifferentiationTagsManager import com.instructure.canvasapi2.managers.graphql.SubmissionCommentsManager import com.instructure.canvasapi2.managers.graphql.SubmissionContentManager import com.instructure.canvasapi2.managers.graphql.SubmissionDetailsManager import com.instructure.canvasapi2.managers.graphql.SubmissionGradeManager +import com.instructure.pandautils.di.DifferentiationTagsModule import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -57,10 +60,11 @@ import org.junit.Test @UninstallModules( GraphQlApiModule::class, - CustomGradeStatusModule::class + CustomGradeStatusModule::class, + DifferentiationTagsModule::class ) @HiltAndroidTest -class SpeedGraderFilesPageTest : TeacherComposeTest() { +class SpeedGraderFilesInteractionTest : TeacherComposeTest() { override fun displaysPageObjects() = Unit @@ -68,6 +72,10 @@ class SpeedGraderFilesPageTest : TeacherComposeTest() { @JvmField val commentLibraryManager: CommentLibraryManager = FakeCommentLibraryManager() + @BindValue + @JvmField + val differentiationTagsManager: DifferentiationTagsManager = FakeDifferentiationTagsManager() + @BindValue @JvmField val postPolicyManager: PostPolicyManager = FakePostPolicyManager() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderGradePageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderGradeInteractionTest.kt similarity index 87% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderGradePageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderGradeInteractionTest.kt index 1e237d8a55..5b3dcdf80f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderGradePageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderGradeInteractionTest.kt @@ -13,28 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import android.util.Log -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.StubMultiAPILevel -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addRubricToAssignment -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCustomGradeStatusesManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeStudentContextManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.annotations.StubMultiAPILevel +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addRubricToAssignment +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCustomGradeStatusesManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeDifferentiationTagsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeStudentContextManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.di.graphql.CustomGradeStatusModule import com.instructure.canvasapi2.managers.CommentLibraryManager @@ -44,10 +45,12 @@ import com.instructure.canvasapi2.managers.StudentContextManager import com.instructure.canvasapi2.managers.SubmissionRubricManager import com.instructure.canvasapi2.managers.graphql.AssignmentDetailsManager import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager +import com.instructure.canvasapi2.managers.graphql.DifferentiationTagsManager import com.instructure.canvasapi2.managers.graphql.SubmissionCommentsManager import com.instructure.canvasapi2.managers.graphql.SubmissionContentManager import com.instructure.canvasapi2.managers.graphql.SubmissionDetailsManager import com.instructure.canvasapi2.managers.graphql.SubmissionGradeManager +import com.instructure.pandautils.di.DifferentiationTagsModule import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.RubricCriterion @@ -58,7 +61,7 @@ import com.instructure.dataseeding.util.ago import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules @@ -67,9 +70,10 @@ import org.junit.Test @HiltAndroidTest @UninstallModules( GraphQlApiModule::class, - CustomGradeStatusModule::class + CustomGradeStatusModule::class, + DifferentiationTagsModule::class ) -class SpeedGraderGradePageTest : TeacherComposeTest() { +class SpeedGraderGradeInteractionTest : TeacherComposeTest() { override fun displaysPageObjects() = Unit @@ -117,9 +121,13 @@ class SpeedGraderGradePageTest : TeacherComposeTest() { @JvmField val customGradeStatusesManager: CustomGradeStatusesManager = FakeCustomGradeStatusesManager() + @BindValue + @JvmField + val differentiationTagsManager: DifferentiationTagsManager = FakeDifferentiationTagsManager() + @Test fun correctViewsForPointGradedWithoutRubric() { - goToSpeedGraderGradePage(gradingType = GradingType.points) + goToSpeedGraderGradePage(gradingType = GradingType.points, score = 10.0, grade = "10") speedGraderGradePage.assertSpeedGraderLabelDisplayed() speedGraderGradePage.assertCurrentEnteredScore("10") @@ -156,14 +164,14 @@ class SpeedGraderGradePageTest : TeacherComposeTest() { speedGraderGradePage.assertFinalGradePointsValueDisplayed("12 / 20 pts") speedGraderGradePage.assertLatePenaltyValueDisplayed("0 pts") - speedGraderGradePage.assertFinalGradeIsDisplayed("12.0") + speedGraderGradePage.assertFinalGradeIsDisplayed("60%") speedGraderGradePage.assertNoRubricCriterionDisplayed() } @Test fun correctViewsForPassFailAssignment() { - goToSpeedGraderGradePage(gradingType = GradingType.pass_fail) + goToSpeedGraderGradePage(gradingType = GradingType.pass_fail, score = 10.0) speedGraderGradePage.assertSpeedGraderLabelDisplayed() speedGraderGradePage.assertCurrentEnteredPassFailScore("10 / 20") @@ -196,7 +204,7 @@ class SpeedGraderGradePageTest : TeacherComposeTest() { @Test fun correctViewsForGpaScaleAssignment() { - goToSpeedGraderGradePage(GradingType.gpa_scale) + goToSpeedGraderGradePage(GradingType.gpa_scale, score = 10.0) speedGraderGradePage.assertSpeedGraderLabelDisplayed() speedGraderGradePage.assertCurrentEnteredScore("10") speedGraderGradePage.assertPointsPossible("20") @@ -217,7 +225,7 @@ class SpeedGraderGradePageTest : TeacherComposeTest() { @Test fun correctViewsForLetterGradeAssignment() { - goToSpeedGraderGradePage(gradingType = GradingType.letter_grade) + goToSpeedGraderGradePage(gradingType = GradingType.letter_grade, score = 10.0) speedGraderGradePage.assertSpeedGraderLabelDisplayed() speedGraderGradePage.assertCurrentEnteredScore("10") @@ -320,6 +328,8 @@ class SpeedGraderGradePageTest : TeacherComposeTest() { gradingType: GradingType = GradingType.points, hasRubric: Boolean = false, pointsPossible: Int = 20, + score: Double = 12.0, + grade: String = "60%", submission: Submission? = null ) { val data = MockCanvas.init(teacherCount = 1, courseCount = 1, favoriteCourseCount = 1, studentCount = 1) @@ -366,8 +376,8 @@ class SpeedGraderGradePageTest : TeacherComposeTest() { userId = student.id, type = "online_text_entry", body = "This is a test submission", - score = 10.0, - grade = "60" + score = score, + grade = grade ) val token = data.tokenFor(teacher)!! diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderInteractionTest.kt similarity index 83% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderInteractionTest.kt index ad514be7e4..2eafd5e2c0 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderInteractionTest.kt @@ -13,24 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addSubmissionsForAssignment -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeStudentContextManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addSubmissionsForAssignment +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeStudentContextManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeDifferentiationTagsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -38,10 +39,12 @@ import com.instructure.canvasapi2.managers.PostPolicyManager import com.instructure.canvasapi2.managers.StudentContextManager import com.instructure.canvasapi2.managers.SubmissionRubricManager import com.instructure.canvasapi2.managers.graphql.AssignmentDetailsManager +import com.instructure.canvasapi2.managers.graphql.DifferentiationTagsManager import com.instructure.canvasapi2.managers.graphql.SubmissionCommentsManager import com.instructure.canvasapi2.managers.graphql.SubmissionContentManager import com.instructure.canvasapi2.managers.graphql.SubmissionDetailsManager import com.instructure.canvasapi2.managers.graphql.SubmissionGradeManager +import com.instructure.pandautils.di.DifferentiationTagsModule import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Assignment.SubmissionType.EXTERNAL_TOOL import com.instructure.canvasapi2.models.Assignment.SubmissionType.ONLINE_TEXT_ENTRY @@ -50,20 +53,24 @@ import com.instructure.canvasapi2.models.Assignment.SubmissionType.ON_PAPER import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules import org.junit.Test @HiltAndroidTest -@UninstallModules(GraphQlApiModule::class) -class SpeedGraderPageTest : TeacherComposeTest() { +@UninstallModules(GraphQlApiModule::class, DifferentiationTagsModule::class) +class SpeedGraderInteractionTest : TeacherComposeTest() { @BindValue @JvmField val commentLibraryManager: CommentLibraryManager = FakeCommentLibraryManager() + @BindValue + @JvmField + val differentiationTagsManager: DifferentiationTagsManager = FakeDifferentiationTagsManager() + @BindValue @JvmField val postPolicyManager: PostPolicyManager = FakePostPolicyManager() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderQuizSubmissionPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderQuizSubmissionInteractionTest.kt similarity index 76% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderQuizSubmissionPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderQuizSubmissionInteractionTest.kt index 158f2b9dae..751cbf56fd 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderQuizSubmissionPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SpeedGraderQuizSubmissionInteractionTest.kt @@ -13,25 +13,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addQuestionToQuiz -import com.instructure.canvas.espresso.mockCanvas.addQuizSubmission -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.annotations.Stub +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addQuestionToQuiz +import com.instructure.canvas.espresso.mockcanvas.addQuizSubmission +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeDifferentiationTagsManager +import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvasapi2.managers.graphql.DifferentiationTagsManager import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Quiz import com.instructure.canvasapi2.models.QuizAnswer +import com.instructure.pandautils.di.DifferentiationTagsModule import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin +import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules import org.junit.Test @HiltAndroidTest -class SpeedGraderQuizSubmissionPageTest : TeacherComposeTest() { +@UninstallModules(DifferentiationTagsModule::class) +class SpeedGraderQuizSubmissionInteractionTest : TeacherComposeTest() { + + @BindValue + @JvmField + val differentiationTagsManager: DifferentiationTagsManager = FakeDifferentiationTagsManager() @Stub @Test diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SyllabusPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SyllabusInteractionTest.kt similarity index 89% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SyllabusPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SyllabusInteractionTest.kt index 4a4f8c2720..a47e95020f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SyllabusPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/SyllabusInteractionTest.kt @@ -14,14 +14,14 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addCourseSettings -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCourseCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addCourseSettings +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.CourseSettings @@ -30,12 +30,12 @@ import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 import com.instructure.teacher.ui.utils.TeacherComposeTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class SyllabusPageTest : TeacherComposeTest() { +class SyllabusInteractionTest : TeacherComposeTest() { override fun displaysPageObjects() = Unit diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCalendarInteractionTest.kt similarity index 83% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCalendarInteractionTest.kt index 2fbcbe6a09..852df550e5 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCalendarInteractionTest.kt @@ -13,25 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import com.instructure.canvas.espresso.common.interaction.CalendarInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.espresso.ModuleItemInteractions import com.instructure.teacher.BuildConfig import com.instructure.teacher.R import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.AssignmentDetailsPage -import com.instructure.teacher.ui.pages.DashboardPage -import com.instructure.teacher.ui.pages.DiscussionsDetailsPage +import com.instructure.teacher.ui.pages.classic.AssignmentDetailsPage +import com.instructure.teacher.ui.pages.classic.DashboardPage +import com.instructure.teacher.ui.pages.classic.DiscussionsDetailsPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest -class TeacherCalendarPageTest : CalendarInteractionTest() { +class TeacherCalendarInteractionTest : CalendarInteractionTest() { override val activityRule = TeacherActivityTestRule(LoginActivity::class.java) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarToDoDetailsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCalendarToDoDetailsInteractionTest.kt similarity index 84% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarToDoDetailsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCalendarToDoDetailsInteractionTest.kt index aa5dbded25..75b4d0c99b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarToDoDetailsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCalendarToDoDetailsInteractionTest.kt @@ -13,23 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import android.app.Activity import com.instructure.canvas.espresso.common.interaction.ToDoDetailsInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.espresso.InstructureActivityTestRule import com.instructure.teacher.BuildConfig import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.DashboardPage +import com.instructure.teacher.ui.pages.classic.DashboardPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest -class TeacherCalendarToDoDetailsPageTest : ToDoDetailsInteractionTest() { +class TeacherCalendarToDoDetailsInteractionTest : ToDoDetailsInteractionTest() { override val activityRule: InstructureActivityTestRule = TeacherActivityTestRule(LoginActivity::class.java) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCreateUpdateEventPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCreateUpdateEventInteractionTest.kt similarity index 86% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCreateUpdateEventPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCreateUpdateEventInteractionTest.kt index ab55465afe..d2905669eb 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCreateUpdateEventPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCreateUpdateEventInteractionTest.kt @@ -13,21 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import com.instructure.canvas.espresso.common.interaction.CreateUpdateEventInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.teacher.BuildConfig import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.DashboardPage +import com.instructure.teacher.ui.pages.classic.DashboardPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest -class TeacherCreateUpdateEventPageTest : CreateUpdateEventInteractionTest() { +class TeacherCreateUpdateEventInteractionTest : CreateUpdateEventInteractionTest() { override val activityRule = TeacherActivityTestRule(LoginActivity::class.java) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCreateUpdateToDoInteractionTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCreateUpdateToDoInteractionTest.kt similarity index 89% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCreateUpdateToDoInteractionTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCreateUpdateToDoInteractionTest.kt index 8558ac33cb..2d22560b3f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCreateUpdateToDoInteractionTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherCreateUpdateToDoInteractionTest.kt @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import com.instructure.canvas.espresso.common.interaction.CreateUpdateToDoInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.User import com.instructure.teacher.BuildConfig import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.DashboardPage +import com.instructure.teacher.ui.pages.classic.DashboardPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherEventDetailsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherEventDetailsInteractionTest.kt similarity index 82% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherEventDetailsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherEventDetailsInteractionTest.kt index 4eb2e46cbf..7d8901a932 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherEventDetailsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherEventDetailsInteractionTest.kt @@ -13,20 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import com.instructure.canvas.espresso.common.interaction.EventDetailsInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.teacher.BuildConfig import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.DashboardPage +import com.instructure.teacher.ui.pages.classic.DashboardPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest -class TeacherEventDetailsPageTest : EventDetailsInteractionTest() { +class TeacherEventDetailsInteractionTest : EventDetailsInteractionTest() { override val activityRule = TeacherActivityTestRule(LoginActivity::class.java) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxComposeInteractionTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxComposeInteractionTest.kt similarity index 85% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxComposeInteractionTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxComposeInteractionTest.kt index a9376f1b81..46a06545e0 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxComposeInteractionTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxComposeInteractionTest.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import androidx.compose.ui.platform.ComposeView import androidx.test.espresso.matcher.ViewMatchers @@ -22,20 +22,20 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.common.interaction.InboxComposeInteractionTest import com.instructure.canvas.espresso.common.pages.InboxPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addRecipientsToCourse -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeStudentContextManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addRecipientsToCourse +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeStudentContextManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -53,9 +53,9 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.User import com.instructure.teacher.BuildConfig import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.DashboardPage +import com.instructure.teacher.ui.pages.classic.DashboardPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxListInteractionTest.kt similarity index 80% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxListPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxListInteractionTest.kt index bd709973b1..5cc8dbd17d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxListPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxListInteractionTest.kt @@ -13,25 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import com.instructure.canvas.espresso.common.interaction.InboxListInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addRecipientsToCourse -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addRecipientsToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.User import com.instructure.teacher.BuildConfig import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.DashboardPage +import com.instructure.teacher.ui.pages.classic.DashboardPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.clickInboxTab -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.clickInboxTab +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @HiltAndroidTest -class TeacherInboxListPageTest : InboxListInteractionTest() { +class TeacherInboxListInteractionTest : InboxListInteractionTest() { override val isTesting = BuildConfig.IS_TESTING override val activityRule = TeacherActivityTestRule(LoginActivity::class.java) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxSignatureInteractionTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxSignatureInteractionTest.kt similarity index 79% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxSignatureInteractionTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxSignatureInteractionTest.kt index d96cd99063..bba43e659b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxSignatureInteractionTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/TeacherInboxSignatureInteractionTest.kt @@ -13,21 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui +package com.instructure.teacher.ui.interaction import com.instructure.canvas.espresso.common.interaction.InboxSignatureInteractionTest -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeAssignmentDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeCommentLibraryManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeInboxSettingsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakePostPolicyManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeStudentContextManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionCommentsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionContentManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionDetailsManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionGradeManager -import com.instructure.canvas.espresso.mockCanvas.fakes.FakeSubmissionRubricManager -import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeAssignmentDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeCommentLibraryManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeInboxSettingsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakePostPolicyManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeStudentContextManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionCommentsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionContentManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionDetailsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionGradeManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeSubmissionRubricManager +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.di.GraphQlApiModule import com.instructure.canvasapi2.managers.CommentLibraryManager import com.instructure.canvasapi2.managers.InboxSettingsManager @@ -41,11 +41,11 @@ import com.instructure.canvasapi2.managers.graphql.SubmissionDetailsManager import com.instructure.canvasapi2.managers.graphql.SubmissionGradeManager import com.instructure.teacher.BuildConfig import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.DashboardPage -import com.instructure.teacher.ui.pages.LeftSideNavigationDrawerPage +import com.instructure.teacher.ui.pages.classic.DashboardPage +import com.instructure.teacher.ui.pages.classic.LeftSideNavigationDrawerPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule -import com.instructure.teacher.ui.utils.openLeftSideMenu -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.openLeftSideMenu +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/UpdateFilePermissionsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/UpdateFilePermissionsInteractionTest.kt similarity index 92% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/UpdateFilePermissionsPageTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/UpdateFilePermissionsInteractionTest.kt index 3fb6f84c2e..e5f4904e46 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/UpdateFilePermissionsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/interaction/UpdateFilePermissionsInteractionTest.kt @@ -16,28 +16,28 @@ * */ -package com.instructure.teacher.ui - -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addItemToModule -import com.instructure.canvas.espresso.mockCanvas.addModuleToCourse -import com.instructure.canvas.espresso.mockCanvas.init +package com.instructure.teacher.ui.interaction + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addItemToModule +import com.instructure.canvas.espresso.mockcanvas.addModuleToCourse +import com.instructure.canvas.espresso.mockcanvas.init import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.ModuleContentDetails import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.util.Randomizer import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.tokenLogin +import com.instructure.teacher.ui.utils.extensions.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.util.Calendar import java.util.Date @HiltAndroidTest -class UpdateFilePermissionsPageTest : TeacherTest() { +class UpdateFilePermissionsInteractionTest : TeacherTest() { override fun displaysPageObjects() = Unit diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AllCoursesListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AllCoursesListPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AllCoursesListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AllCoursesListPage.kt index 8c5ee233cc..c928ce451d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AllCoursesListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AllCoursesListPage.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.view.View import com.instructure.canvasapi2.models.Course diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AnnouncementsListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AnnouncementsListPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AnnouncementsListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AnnouncementsListPage.kt index 932b13e31f..5882237770 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AnnouncementsListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AnnouncementsListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.assertion.ViewAssertions diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssigneeListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssigneeListPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssigneeListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssigneeListPage.kt index 51faf5e03d..e8f754dbdd 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssigneeListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssigneeListPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.recyclerview.widget.RecyclerView diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssignmentDetailsPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssignmentDetailsPage.kt index 62cafea34c..fd9a5cdabc 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssignmentDetailsPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.annotation.StringRes diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDueDatesPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssignmentDueDatesPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDueDatesPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssignmentDueDatesPage.kt index 6390d9135a..360397797b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentDueDatesPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/AssignmentDueDatesPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import com.instructure.espresso.OnViewWithContentDescription diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CommentLibraryPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CommentLibraryPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CommentLibraryPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CommentLibraryPage.kt index 1f1d2183a0..78bd8f09c4 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CommentLibraryPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CommentLibraryPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import com.instructure.espresso.OnViewWithId import com.instructure.espresso.RecyclerViewItemCountAssertion diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseBrowserPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CourseBrowserPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseBrowserPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CourseBrowserPage.kt index 038f29690f..a8311a954a 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseBrowserPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CourseBrowserPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.NoMatchingViewException diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseSettingsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CourseSettingsPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseSettingsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CourseSettingsPage.kt index b01492d45b..4cfe0ff5cc 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseSettingsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/CourseSettingsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DashboardPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DashboardPage.kt index 85b2d89c00..f2fb327d64 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DashboardPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.view.View import androidx.test.espresso.assertion.ViewAssertions.doesNotExist diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DiscussionsDetailsPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DiscussionsDetailsPage.kt index 155564113d..3cfebae854 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DiscussionsDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.web.assertion.WebViewAssertions diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DiscussionsListPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DiscussionsListPage.kt index dee91ab3f9..8bcf42c3b9 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/DiscussionsListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAnnouncementDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditAnnouncementDetailsPage.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAnnouncementDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditAnnouncementDetailsPage.kt index 9415744a68..a3c704e58d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAnnouncementDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditAnnouncementDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.Espresso import com.instructure.espresso.click diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAssignmentDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditAssignmentDetailsPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAssignmentDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditAssignmentDetailsPage.kt index e58ed863aa..686284c2b3 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAssignmentDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditAssignmentDetailsPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.widget.DatePicker diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDashboardPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditDashboardPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDashboardPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditDashboardPage.kt index aaee4926e4..5aa62ddac3 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDashboardPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditDashboardPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.assertion.ViewAssertions.matches diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDiscussionsDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditDiscussionsDetailsPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDiscussionsDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditDiscussionsDetailsPage.kt index e89ad6cc21..8d946573db 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDiscussionsDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditDiscussionsDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.Espresso import com.instructure.espresso.WaitForViewWithId diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditPageDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditPageDetailsPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditPageDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditPageDetailsPage.kt index 6f89f22fd8..126611630d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditPageDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditPageDetailsPage.kt @@ -1,4 +1,4 @@ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditProfileSettingsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditProfileSettingsPage.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditProfileSettingsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditProfileSettingsPage.kt index 2176737592..4f5c9f008b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditProfileSettingsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditProfileSettingsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import com.instructure.espresso.OnViewWithId import com.instructure.espresso.clearText diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditQuizDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditQuizDetailsPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditQuizDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditQuizDetailsPage.kt index 5f575a0846..9db7b2eb49 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditQuizDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditQuizDetailsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.widget.DatePicker diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditSyllabusPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditSyllabusPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditSyllabusPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditSyllabusPage.kt index 0723ba0e80..67203f5c21 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditSyllabusPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/EditSyllabusPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/FileListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/FileListPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/FileListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/FileListPage.kt index 3cd71b51e4..b343143c9f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/FileListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/FileListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.appcompat.widget.AppCompatButton import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/HelpPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/HelpPage.kt similarity index 85% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/HelpPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/HelpPage.kt index 979bbd0f3b..2609b28e5f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/HelpPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/HelpPage.kt @@ -14,25 +14,20 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.app.Instrumentation import android.content.Intent import androidx.test.espresso.Espresso -import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.matcher.IntentMatchers -import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.withCustomConstraints -import com.instructure.canvasapi2.models.Course import com.instructure.espresso.OnViewWithStringTextIgnoreCase import com.instructure.espresso.OnViewWithText import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click -import com.instructure.espresso.matchers.WaitForViewMatcher.waitForView import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.onView import com.instructure.espresso.page.plus @@ -43,16 +38,11 @@ import com.instructure.teacher.R import org.hamcrest.CoreMatchers /** - * A page representing the Help menu in the application. + * A page representing the Help menu in the Teacher application. * */ class HelpPage : BasePage(R.id.helpDialog) { - /** - * The label for asking an instructor. - */ - private val askInstructorLabel by OnViewWithText(R.string.askInstructor) - /** * The label for searching guides. */ @@ -61,7 +51,7 @@ class HelpPage : BasePage(R.id.helpDialog) { /** * The label for reporting a problem. */ - private val reportProblemLabel by OnViewWithText(R.string.reportProblem) + private val reportProblemLabel by OnViewWithText(R.string.report_problem) /** * The label for submitting a feature idea. @@ -73,34 +63,20 @@ class HelpPage : BasePage(R.id.helpDialog) { */ private val shareLoveLabel by OnViewWithText(R.string.shareYourLove) - /** - * Verifies asking a question to an instructor. - * - * @param course The course to select in the spinner. - * @param question The question to type in the message field. - */ - fun verifyAskAQuestion(course: Course, question: String) { - askInstructorLabel.scrollTo().click() - waitForView(withText(course.name)).assertDisplayed() - onView(withId(R.id.message)).scrollTo().perform(withCustomConstraints(typeText(question), isDisplayingAtLeast(1))) - Espresso.closeSoftKeyboard() - onView(containsTextCaseInsensitive("Send")).assertDisplayed() - } - /** * Clicks on the 'Search the Canvas Guides' help menu. */ - fun clickSearchGuidesLabel() { + private fun clickSearchGuidesLabel() { searchGuidesLabel.scrollTo().click() } /** - * Verifies reporting a problem. + * Asserts the details of the 'Report a problem' dialog by filling in the subject and description fields and checking for the 'Send' button. * * @param subject The subject of the problem. * @param description The description of the problem. */ - fun verifyReportAProblem(subject: String, description: String) { + fun assertReportProblemDialogDetails(subject: String, description: String) { reportProblemLabel.scrollTo().click() onView(withId(R.id.subjectEditText)).typeText(subject) Espresso.closeSoftKeyboard() @@ -109,6 +85,9 @@ class HelpPage : BasePage(R.id.helpDialog) { onView(containsTextCaseInsensitive("Send")).scrollTo().assertDisplayed() } + /** + * Clicks on the 'Send' button on the 'Report a problem' dialog. + */ fun clickSendReportProblem() { onView(containsTextCaseInsensitive("Send")).scrollTo().click() } @@ -200,6 +179,12 @@ class HelpPage : BasePage(R.id.helpDialog) { onView(withId(R.id.subtitle) + withText("Tell us about your favorite parts of the app")).assertDisplayed() } + /** + * Asserts that clicking on a Help menu item launches an intent with the expected URL. + * + * @param helpMenuText The text of the Help menu item to click. + * @param expectedURL The expected URL that should be opened. + */ fun assertHelpMenuURL(helpMenuText: String, expectedURL: String) { val expectedIntent = CoreMatchers.allOf( IntentMatchers.hasAction(Intent.ACTION_VIEW), diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LeftSideNavigationDrawerPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/LeftSideNavigationDrawerPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LeftSideNavigationDrawerPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/LeftSideNavigationDrawerPage.kt index 89bcbc6e8e..6c0d83251a 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LeftSideNavigationDrawerPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/LeftSideNavigationDrawerPage.kt @@ -1,4 +1,4 @@ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.view.View import androidx.appcompat.widget.SwitchCompat diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ModulesPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/ModulesPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ModulesPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/ModulesPage.kt index 8b1cd6c598..77abf532fd 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ModulesPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/ModulesPage.kt @@ -1,4 +1,4 @@ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.annotation.StringRes import androidx.test.espresso.matcher.ViewMatchers.hasSibling diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NavDrawerPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/NavDrawerPage.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NavDrawerPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/NavDrawerPage.kt index f4159fe6c8..84c89ea0aa 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NavDrawerPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/NavDrawerPage.kt @@ -1,4 +1,4 @@ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.withText diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NotATeacherPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/NotATeacherPage.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NotATeacherPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/NotATeacherPage.kt index d1ce0395c7..feb972d826 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NotATeacherPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/NotATeacherPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import com.instructure.espresso.WaitForViewWithId import com.instructure.espresso.click diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PageListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PageListPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PageListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PageListPage.kt index a42d4c9639..e9578656ae 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PageListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PageListPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.view.View import androidx.test.espresso.Espresso diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PeopleListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PeopleListPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PeopleListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PeopleListPage.kt index bdb4cef085..53b312827d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PeopleListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PeopleListPage.kt @@ -12,7 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */ package com.instructure.teacher.ui.pages + */ package com.instructure.teacher.ui.pages.classic import android.view.View import androidx.recyclerview.widget.RecyclerView diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PersonContextPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PersonContextPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PersonContextPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PersonContextPage.kt index 5c6775ad4c..bd4a8c3dbb 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PersonContextPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PersonContextPage.kt @@ -15,7 +15,7 @@ */ @file:Suppress("unused") -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.matches import com.instructure.canvas.espresso.containsTextCaseInsensitive diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PostSettingsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PostSettingsPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PostSettingsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PostSettingsPage.kt index 57f4030342..c12de307a8 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PostSettingsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PostSettingsPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import com.instructure.espresso.WaitForViewWithId diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ProfileSettingsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/ProfileSettingsPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ProfileSettingsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/ProfileSettingsPage.kt index 5c9e51b82b..be10fdb00b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ProfileSettingsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/ProfileSettingsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.assertion.ViewAssertions.matches import com.instructure.espresso.OnViewWithId diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PushNotificationsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PushNotificationsPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PushNotificationsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PushNotificationsPage.kt index 612a39f914..1e81412e11 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PushNotificationsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/PushNotificationsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizDetailsPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizDetailsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizDetailsPage.kt index 7f683cdb4c..0deec368ca 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizDetailsPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.InstrumentationRegistry import com.instructure.canvasapi2.models.Quiz diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizListPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizListPage.kt index 5898120932..faacf6f206 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/QuizListPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import com.instructure.canvasapi2.models.Quiz import com.instructure.espresso.DoesNotExistAssertion diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/RemoteConfigSettingsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/RemoteConfigSettingsPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/RemoteConfigSettingsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/RemoteConfigSettingsPage.kt index 284274d18e..c94595441f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/RemoteConfigSettingsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/RemoteConfigSettingsPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.view.View import android.widget.EditText diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderCommentsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SpeedGraderCommentsPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderCommentsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SpeedGraderCommentsPage.kt index 4948351dad..4dba8ed42a 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderCommentsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SpeedGraderCommentsPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers.Visibility diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderQuizSubmissionPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SpeedGraderQuizSubmissionPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderQuizSubmissionPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SpeedGraderQuizSubmissionPage.kt index 640dad24e1..00983dfdf3 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderQuizSubmissionPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SpeedGraderQuizSubmissionPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.web.sugar.Web import androidx.test.espresso.web.webdriver.DriverAtoms diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/StudentContextPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/StudentContextPage.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/StudentContextPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/StudentContextPage.kt index 706ba1165f..2809cf3af9 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/StudentContextPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/StudentContextPage.kt @@ -15,9 +15,8 @@ */ @file:Suppress("unused") -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic -import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.espresso.WaitForViewWithId import com.instructure.espresso.assertDisplayed import com.instructure.espresso.assertHasText diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SyllabusPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SyllabusPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SyllabusPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SyllabusPage.kt index df93e38cf4..5351c68628 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SyllabusPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/SyllabusPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.app.Activity import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/TodoPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/TodoPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/TodoPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/TodoPage.kt index e27e701eef..59c7ae8d3c 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/TodoPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/TodoPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.matcher.ViewMatchers.hasSibling import com.instructure.espresso.RecyclerViewItemCountAssertion diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/UpdateFilePermissionsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/UpdateFilePermissionsPage.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/UpdateFilePermissionsPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/UpdateFilePermissionsPage.kt index 4527b07dd1..da7a148e8d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/UpdateFilePermissionsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/UpdateFilePermissionsPage.kt @@ -16,7 +16,7 @@ * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import android.widget.DatePicker import android.widget.TimePicker diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/WebViewLoginPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/WebViewLoginPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/WebViewLoginPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/WebViewLoginPage.kt index 8559af1316..aa40a009e8 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/WebViewLoginPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/classic/WebViewLoginPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.classic import androidx.test.espresso.web.sugar.Web import androidx.test.espresso.web.sugar.Web.onWebView diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentSubmissionListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/AssignmentSubmissionListPage.kt similarity index 61% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentSubmissionListPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/AssignmentSubmissionListPage.kt index bc22c3523f..af4ac0c515 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentSubmissionListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/AssignmentSubmissionListPage.kt @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.compose +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasAnyChild @@ -63,7 +65,10 @@ class AssignmentSubmissionListPage(private val composeTestRule: ComposeTestRule) * @param canvasUser The Canvas user whose submission is to be verified (based on the user's name). */ fun assertHasStudentSubmission(canvasUser: CanvasUserApiModel) { - composeTestRule.onNode(hasTestTag("submissionListItemStudentName") and hasText(canvasUser.name), useUnmergedTree = true) + composeTestRule.onNode( + hasTestTag("submissionListItemStudentName") and hasText(canvasUser.name), + useUnmergedTree = true + ) .performScrollTo() .assertIsDisplayed() } @@ -74,7 +79,10 @@ class AssignmentSubmissionListPage(private val composeTestRule: ComposeTestRule) * @param canvasUser The Canvas user whose submission is to be verified (based on the user's name). */ fun assertStudentSubmissionNotDisplayed(canvasUser: CanvasUserApiModel) { - composeTestRule.onNode(hasTestTag("submissionListItemStudentName") and hasText(canvasUser.name), useUnmergedTree = true) + composeTestRule.onNode( + hasTestTag("submissionListItemStudentName") and hasText(canvasUser.name), + useUnmergedTree = true + ) .assertDoesNotExist() } @@ -97,22 +105,6 @@ class AssignmentSubmissionListPage(private val composeTestRule: ComposeTestRule) composeTestRule.onNodeWithTag("clearButton").performClick() } - /** - * Assert filter label all submissions - * - */ - fun assertFilterLabelAllSubmissions() { - composeTestRule.onNodeWithText("All Submissions").assertIsDisplayed() - } - - /** - * Assert filter label 'Haven't Submitted Yet' (aka. 'Not Submitted') - * - */ - fun assertFilterLabelNotSubmittedSubmissions() { - composeTestRule.onNodeWithText("Haven't Submitted Yet").assertIsDisplayed() - } - /** * Assert that the scoreText is displayed besides the proper student. * @@ -120,13 +112,13 @@ class AssignmentSubmissionListPage(private val composeTestRule: ComposeTestRule) fun assertStudentScoreText(studentName: String, scoreText: String) { composeTestRule.onNode( - hasTestTag("scoreText") and hasText(scoreText) and( - hasParent( - hasTestTag("submissionListItem").and( - hasAnyDescendant(hasText(studentName) and hasTestTag("submissionListItemStudentName")) + hasTestTag("scoreText") and hasText(scoreText) and ( + hasParent( + hasTestTag("submissionListItem").and( + hasAnyDescendant(hasText(studentName) and hasTestTag("submissionListItemStudentName")) + ) ) - ) - ), useUnmergedTree = true + ), useUnmergedTree = true ).assertIsDisplayed() } @@ -152,7 +144,9 @@ class AssignmentSubmissionListPage(private val composeTestRule: ComposeTestRule) * * @param student */ + @OptIn(ExperimentalTestApi::class) fun clickSubmission(student: CanvasUserApiModel) { + composeTestRule.waitUntilExactlyOneExists(hasText(student.name), timeoutMillis = 5000) composeTestRule.onNodeWithText(student.name, useUnmergedTree = true) .performScrollTo() .performClick() @@ -178,7 +172,10 @@ class AssignmentSubmissionListPage(private val composeTestRule: ComposeTestRule) * */ fun clickFilterSubmittedLate() { - composeTestRule.onNodeWithText("Submitted Late", useUnmergedTree = true) + composeTestRule.onNode( + hasTestTag("statusCheckBox").and(hasAnySibling(hasText("Late"))), + useUnmergedTree = true + ) .performScrollTo() .performClick() } @@ -189,11 +186,9 @@ class AssignmentSubmissionListPage(private val composeTestRule: ComposeTestRule) */ fun clickFilterUngraded() { composeTestRule.onNode( - hasTestTag("filterItem") - .and(hasAnyChild(hasText("Needs Grading"))), + hasTestTag("statusCheckBox") and hasAnySibling(hasText("Needs Grading")), useUnmergedTree = true - ) - .performScrollTo() + ).performScrollTo() .performClick() } @@ -203,21 +198,22 @@ class AssignmentSubmissionListPage(private val composeTestRule: ComposeTestRule) */ fun assertSubmissionFilterOption(filterName: String) { composeTestRule.onNode( - hasTestTag("filterItem") - .and(hasAnyChild(hasText(filterName))), + hasTestTag("statusCheckBox") and hasAnySibling(hasText(filterName)), useUnmergedTree = true - ).assertIsDisplayed() + ).performScrollTo().assertIsDisplayed() } - /** - * Assert filter label text - * - * @param text - */ - fun assertFilterLabelText(text: String) { - composeTestRule.onNodeWithText(text).assertIsDisplayed() + fun assertCustomStatusFilterOption(filterName: String) { + composeTestRule.onNode( + hasTestTag("customStatusCheckBox") and hasAnySibling(hasText(filterName)), + useUnmergedTree = true + ).performScrollTo().assertIsDisplayed() } + fun assertPreciseFilterOption(filterName: String) { + composeTestRule.onNode(hasText(filterName), useUnmergedTree = true).performScrollTo() + .assertIsDisplayed() + } /** * Assert has submission @@ -291,7 +287,8 @@ class AssignmentSubmissionListPage(private val composeTestRule: ComposeTestRule) Clicks on the "Done" button in the filter dialog. */ fun clickFilterDialogDone() { - composeTestRule.onNode(hasTestTag("appBarDoneButton"), useUnmergedTree = true).performClick() + composeTestRule.onNode(hasTestTag("appBarDoneButton"), useUnmergedTree = true) + .performClick() composeTestRule.waitForIdle() } @@ -301,7 +298,10 @@ class AssignmentSubmissionListPage(private val composeTestRule: ComposeTestRule) * @param name The name of the section to filter by. */ fun filterBySection(name: String) { - composeTestRule.onNode(hasTestTag("sectionCheckBox") and hasAnySibling(hasText(name)), useUnmergedTree = true) + composeTestRule.onNode( + hasTestTag("sectionCheckBox") and hasAnySibling(hasText(name)), + useUnmergedTree = true + ) .performScrollTo() .performClick() composeTestRule.waitForIdle() @@ -317,7 +317,7 @@ class AssignmentSubmissionListPage(private val composeTestRule: ComposeTestRule) hasTestTag("hiddenIcon").and( hasParent( hasTestTag("submissionListItem").and( - hasAnyChild(hasText(studentName)) + hasAnyDescendant(hasText(studentName) and hasTestTag("submissionListItemStudentName")) ) ) ), useUnmergedTree = true @@ -326,12 +326,31 @@ class AssignmentSubmissionListPage(private val composeTestRule: ComposeTestRule) .assertIsDisplayed() } + /** + * Click on the 'Submitted' filter option. + * + */ + fun clickFilterSubmitted() { + composeTestRule.onNode( + hasTestTag("statusCheckBox").and(hasAnySibling(hasText("Submitted"))), + useUnmergedTree = true + ) + .performScrollTo() + .performClick() + } + /** * Click on the 'Not Submitted' filter option. * */ fun clickFilterNotSubmitted() { - composeTestRule.onNodeWithText("Not Submitted", useUnmergedTree = true) + // Note: In the actual filter screen, "Not Submitted" status doesn't exist + // The statuses are: Late, Missing, Needs Grading, Graded, Submitted + // This method may need to be updated based on the actual filter available + composeTestRule.onNode( + hasTestTag("statusCheckBox").and(hasAnySibling(hasText("Missing"))), + useUnmergedTree = true + ) .performScrollTo() .performClick() } @@ -365,4 +384,101 @@ class AssignmentSubmissionListPage(private val composeTestRule: ComposeTestRule) swipeDown() } } + + /** + * Click on a sort order option. + * + * @param sortOrderName The name of the sort order option to click (e.g., "Submission Date", "Student Name"). + */ + fun clickSortOrder(sortOrderName: String) { + composeTestRule.onNodeWithText(sortOrderName, useUnmergedTree = true) + .performScrollTo() + .performClick() + } + + /** + * Assert that submissions are displayed in the expected order. + * + * @param studentNames List of student names in the expected display order. + */ + fun assertSubmissionsInOrder(studentNames: List) { + val submissionNodes = composeTestRule.onAllNodes( + hasTestTag("submissionListItem"), + useUnmergedTree = true + ) + + studentNames.forEachIndexed { index, name -> + submissionNodes[index] + .assert(hasAnyDescendant(hasText(name))) + } + } + + /** + * Click on a custom grade status filter option. + * + * @param statusName The name of the custom status to filter by. + */ + fun clickFilterCustomStatus(statusName: String) { + composeTestRule.onNode( + hasTestTag("customStatusCheckBox").and(hasAnySibling(hasText(statusName))), + useUnmergedTree = true + ) + .performScrollTo() + .performClick() + composeTestRule.waitForIdle() + } + + /** + * Assert that a custom status tag is displayed on a submission. + * + * @param statusName The name of the custom status tag to verify. + */ + fun assertCustomStatusTag(statusName: String) { + composeTestRule.onNodeWithText(statusName, useUnmergedTree = true) + .performScrollTo() + .assertIsDisplayed() + } + + /** + * Click on a differentiation tag filter option. + * + * @param tagName The name of the differentiation tag to filter by. + */ + fun clickFilterDifferentiationTag(tagName: String) { + // Click on the text element with the tag name that has a checkbox sibling + // Use [0] to get the first match since there may be multiple text nodes in the tree + composeTestRule.onAllNodes( + hasText(tagName).and( + hasAnySibling(hasTestTag("differentiationTagCheckBox")) + ), + useUnmergedTree = true + )[0] + .performScrollTo() + .performClick() + composeTestRule.waitForIdle() + } + + /** + * Click on the "Include students without differentiation tags" checkbox. + */ + fun clickIncludeStudentsWithoutTags() { + composeTestRule.onNode( + hasTestTag("includeWithoutTagsCheckBox"), + useUnmergedTree = true + ) + .performScrollTo() + .performClick() + composeTestRule.waitForIdle() + } + + /** + * Assert that a differentiation tag is displayed on a submission. + * + * @param tagName The name of the differentiation tag to verify. + */ + fun assertDifferentiationTag(tagName: String) { + composeTestRule.onNodeWithText(tagName, useUnmergedTree = true) + .performScrollTo() + .assertIsDisplayed() + } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ProgressPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/ProgressPage.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ProgressPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/ProgressPage.kt index c357b1157d..3d419dcfd6 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ProgressPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/ProgressPage.kt @@ -16,7 +16,7 @@ * */ -package com.instructure.teacher.ui.pages +package com.instructure.teacher.ui.pages.compose import androidx.annotation.StringRes import androidx.compose.ui.test.ExperimentalTestApi diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderGradePage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderGradePage.kt index 9cf768c1d2..78ab0a180d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderGradePage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderGradePage.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextReplacement import androidx.test.espresso.Espresso -import com.instructure.composeTest.hasTestTagThatContains +import com.instructure.composetest.hasTestTagThatContains import com.instructure.espresso.OnViewWithId import com.instructure.espresso.OnViewWithText import com.instructure.espresso.WaitForViewWithId diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderPage.kt index 4f267665dd..7ba1eec724 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/compose/SpeedGraderPage.kt @@ -16,7 +16,9 @@ package com.instructure.teacher.ui.pages.compose import androidx.annotation.StringRes +import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertTextContains import androidx.compose.ui.test.hasAnyAncestor @@ -24,7 +26,7 @@ import androidx.compose.ui.test.hasAnySibling import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onChildren +import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -39,7 +41,7 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.models.User -import com.instructure.composeTest.hasTestTagThatContains +import com.instructure.composetest.hasTestTagThatContains import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.SubmissionApiModel import com.instructure.espresso.OnViewWithId @@ -66,7 +68,6 @@ import com.instructure.espresso.swipeToTop import com.instructure.teacher.R import org.hamcrest.Matchers import org.hamcrest.Matchers.allOf -import org.junit.Assert.assertEquals import java.util.Locale /** @@ -95,9 +96,11 @@ class SpeedGraderPage(private val composeTestRule: ComposeTestRule) : BasePage() /** * Clicks the expand panel button in the Compose UI. */ + @OptIn(ExperimentalTestApi::class) fun clickExpandPanelButton() { composeTestRule - .onNodeWithTag("expandPanelButton", useUnmergedTree = true) + .waitUntilExactlyOneExists(hasTestTag("expandPanelButton"), timeoutMillis = 10000) + composeTestRule.onNodeWithTag("expandPanelButton", useUnmergedTree = true) .performClick() composeTestRule.waitForIdle() } @@ -252,15 +255,17 @@ class SpeedGraderPage(private val composeTestRule: ComposeTestRule) : BasePage() */ fun selectCommentLibraryResultItem(index: Int? = null) { - val textNodes = composeTestRule - .onNodeWithTag("commentLibraryListColumn") - .onChildren() - .fetchSemanticsNodes() - - val targetText = textNodes[index ?: 0] - .config[androidx.compose.ui.semantics.SemanticsProperties.Text] + val targetText = composeTestRule + .onAllNodesWithTag("commentLibraryItem")[index ?: 0] + .fetchSemanticsNode() + .config[SemanticsProperties.Text] .joinToString("") { it.text } - composeTestRule.onNode(hasText(targetText, substring = true) and !(hasTestTag("ownCommentText"))) + composeTestRule.onNode( + hasText( + targetText, + substring = true + ) and (hasTestTag("ownCommentText").not()) + ) .performScrollTo() .performClick() composeTestRule.waitForIdle() @@ -290,12 +295,9 @@ class SpeedGraderPage(private val composeTestRule: ComposeTestRule) : BasePage() @OptIn(ExperimentalTestApi::class) fun assertCommentLibraryItemCount(expectedCount: Int) { composeTestRule.waitUntilExactlyOneExists(hasTestTagThatContains("commentLibraryListColumn"), timeoutMillis = 5000) - val textNodes = composeTestRule - .onNodeWithTag("commentLibraryListColumn") - .onChildren() - .fetchSemanticsNodes() - - assertEquals(expectedCount, textNodes.size) + composeTestRule + .onAllNodesWithTag("commentLibraryItem") + .assertCountEquals(expectedCount) } /** @@ -359,7 +361,7 @@ class SpeedGraderPage(private val composeTestRule: ComposeTestRule) : BasePage() getStringFromResource( R.string.sg_tab_files_w_counter, fileCount - ).toUpperCase() + ).uppercase() ) filesTab.click() } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/EditSyllabusRenderTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/EditSyllabusRenderTest.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/EditSyllabusRenderTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/EditSyllabusRenderTest.kt index df7fc5f00d..2d88bc6a21 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/EditSyllabusRenderTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/EditSyllabusRenderTest.kt @@ -14,12 +14,12 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.renderTests +package com.instructure.teacher.ui.rendertests import com.instructure.canvasapi2.models.Course import com.instructure.teacher.features.syllabus.edit.EditSyllabusFragment import com.instructure.teacher.features.syllabus.edit.EditSyllabusViewState -import com.instructure.teacher.ui.renderTests.pages.EditSyllabusRenderPage +import com.instructure.teacher.ui.rendertests.renderpages.EditSyllabusRenderPage import com.instructure.teacher.ui.utils.TeacherRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/ModuleListRenderTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/ModuleListRenderTest.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/ModuleListRenderTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/ModuleListRenderTest.kt index 31bf453bbf..e8c3910048 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/ModuleListRenderTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/ModuleListRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.renderTests +package com.instructure.teacher.ui.rendertests import android.graphics.Color import androidx.test.espresso.assertion.ViewAssertions.matches @@ -32,7 +32,7 @@ import com.instructure.teacher.R import com.instructure.teacher.features.modules.list.ui.ModuleListFragment import com.instructure.teacher.features.modules.list.ui.ModuleListItemData import com.instructure.teacher.features.modules.list.ui.ModuleListViewState -import com.instructure.teacher.ui.renderTests.pages.ModuleListRenderPage +import com.instructure.teacher.ui.rendertests.renderpages.ModuleListRenderPage import com.instructure.teacher.ui.utils.TeacherRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/PostGradeRenderTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/PostGradeRenderTest.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/PostGradeRenderTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/PostGradeRenderTest.kt index 4265baf5f8..9074d3a1db 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/PostGradeRenderTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/PostGradeRenderTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.renderTests +package com.instructure.teacher.ui.rendertests import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Section @@ -22,7 +22,7 @@ import com.instructure.teacher.R import com.instructure.teacher.features.postpolicies.PostSection import com.instructure.teacher.features.postpolicies.ui.PostGradeFragment import com.instructure.teacher.features.postpolicies.ui.PostGradeViewState -import com.instructure.teacher.ui.renderTests.pages.PostGradeRenderPage +import com.instructure.teacher.ui.rendertests.renderpages.PostGradeRenderPage import com.instructure.teacher.ui.utils.TeacherRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/SyllabusRenderTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/SyllabusRenderTest.kt similarity index 93% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/SyllabusRenderTest.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/SyllabusRenderTest.kt index 448872748b..e00956492e 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/SyllabusRenderTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/SyllabusRenderTest.kt @@ -14,16 +14,16 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.renderTests +package com.instructure.teacher.ui.rendertests -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.annotations.Stub import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.teacher.features.syllabus.SyllabusModel -import com.instructure.teacher.features.syllabus.ui.SyllabusFragment -import com.instructure.teacher.ui.renderTests.pages.SyllabusRenderPage +import com.instructure.teacher.features.syllabus.ui.SyllabusRepositoryFragment +import com.instructure.teacher.ui.rendertests.renderpages.SyllabusRenderPage import com.instructure.teacher.ui.utils.TeacherRenderTest import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest @@ -147,7 +147,7 @@ class SyllabusRenderTest : TeacherRenderTest() { override fun post(runnable: Runnable) = Unit } val canvasContext = model.course?.dataOrNull ?: Course(id = model.courseId) - val fragment = SyllabusFragment.newInstance(canvasContext)!!.apply { + val fragment = SyllabusRepositoryFragment.newInstance(canvasContext)!!.apply { overrideInitModel = model loopMod = { it.effectRunner { emptyEffectRunner } } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/EditSyllabusRenderPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/EditSyllabusRenderPage.kt similarity index 98% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/EditSyllabusRenderPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/EditSyllabusRenderPage.kt index 1936839aaa..88f64cc57a 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/EditSyllabusRenderPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/EditSyllabusRenderPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.renderTests.pages +package com.instructure.teacher.ui.rendertests.renderpages import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/ModuleListRenderPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/ModuleListRenderPage.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/ModuleListRenderPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/ModuleListRenderPage.kt index 7fa38d027c..d448abd6f3 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/ModuleListRenderPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/ModuleListRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.renderTests.pages +package com.instructure.teacher.ui.rendertests.renderpages import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView @@ -21,6 +21,8 @@ import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import com.instructure.canvas.espresso.SwipeRefreshLayoutMatchers +import com.instructure.canvas.espresso.ViewSizeMatcher import com.instructure.espresso.OnViewWithId import com.instructure.espresso.RecyclerViewItemCountAssertion import com.instructure.espresso.assertDisplayed @@ -29,8 +31,6 @@ import com.instructure.espresso.page.onView import com.instructure.espresso.page.withAncestor import com.instructure.espresso.page.withText import com.instructure.teacher.R -import com.instructure.teacher.ui.utils.SwipeRefreshLayoutMatchers -import com.instructure.teacher.ui.utils.ViewSizeMatcher import org.hamcrest.CoreMatchers.allOf class ModuleListRenderPage : BasePage(R.id.moduleList) { diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/PostGradeRenderPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/PostGradeRenderPage.kt similarity index 97% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/PostGradeRenderPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/PostGradeRenderPage.kt index 79394d11fd..3096516a58 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/PostGradeRenderPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/PostGradeRenderPage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.renderTests.pages +package com.instructure.teacher.ui.rendertests.renderpages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/SyllabusRenderPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/SyllabusRenderPage.kt similarity index 95% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/SyllabusRenderPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/SyllabusRenderPage.kt index 73d20f7ebc..3a21a3d01d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/pages/SyllabusRenderPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/rendertests/renderpages/SyllabusRenderPage.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.teacher.ui.renderTests.pages +package com.instructure.teacher.ui.rendertests.renderpages import androidx.test.espresso.action.ViewActions import com.instructure.espresso.OnViewWithId @@ -28,7 +28,7 @@ import com.instructure.espresso.page.withId import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.teacher.R -import com.instructure.teacher.ui.pages.SyllabusPage +import com.instructure.teacher.ui.pages.classic.SyllabusPage import org.hamcrest.CoreMatchers import org.hamcrest.Matchers diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/Matchers.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/Matchers.kt deleted file mode 100644 index eb5c6ac8bc..0000000000 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/Matchers.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2019 - present Instructure, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.instructure.teacher.ui.utils - -import android.view.View -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import androidx.test.espresso.matcher.BoundedMatcher -import org.hamcrest.Description -import org.hamcrest.Matcher -import org.hamcrest.TypeSafeMatcher - -object SwipeRefreshLayoutMatchers { - fun isRefreshing(isRefreshing: Boolean): Matcher { - return object : BoundedMatcher(SwipeRefreshLayout::class.java) { - - override fun describeTo(description: Description) { - description.appendText(if (isRefreshing) "is refreshing" else "is not refreshing") - } - - override fun matchesSafely(view: SwipeRefreshLayout): Boolean { - return view.isRefreshing == isRefreshing - } - } - } -} - -object ViewSizeMatcher { - fun hasWidth(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { - override fun describeTo(description: Description) { - description.appendText("has a width of ${pixels}px") - } - - override fun matchesSafely(view: View): Boolean = view.width == pixels - } - - fun hasHeight(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { - override fun describeTo(description: Description) { - description.appendText("has a height of ${pixels}px") - } - - override fun matchesSafely(view: View): Boolean = view.height == pixels - } - - fun hasMinWidth(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { - override fun describeTo(description: Description) { - description.appendText("has a minimum width of ${pixels}px") - } - - override fun matchesSafely(view: View): Boolean = view.width >= pixels - } - - fun hasMinHeight(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { - override fun describeTo(description: Description) { - description.appendText("has a minimum height of ${pixels}px") - } - - override fun matchesSafely(view: View): Boolean = view.height >= pixels - } -} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherActivityTestRule.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherActivityTestRule.kt index aa47deb4fc..9a5d28b3c1 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherActivityTestRule.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherActivityTestRule.kt @@ -22,7 +22,6 @@ import com.instructure.espresso.InstructureActivityTestRule import com.instructure.loginapi.login.util.LoginPrefs import com.instructure.loginapi.login.util.PreviousUsersUtils import com.instructure.pandautils.utils.PandaAppResetter -import com.instructure.pandautils.utils.ThemePrefs import com.instructure.teacher.utils.TeacherPrefs class TeacherActivityTestRule(activityClass: Class) : InstructureActivityTestRule(activityClass) { @@ -32,9 +31,6 @@ class TeacherActivityTestRule(activityClass: Class) : Instructur TeacherPrefs.safeClearPrefs() PreviousUsersUtils.clear(context) LoginPrefs.clearPrefs() - - // We need to set this true so the theme selector won't stop our tests. - ThemePrefs.themeSelectionShown = true } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherComposeTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherComposeTest.kt index 3811148952..ffa88fdb1e 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherComposeTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherComposeTest.kt @@ -33,8 +33,8 @@ import com.instructure.canvas.espresso.common.pages.compose.RecipientPickerPage import com.instructure.canvas.espresso.common.pages.compose.SelectContextPage import com.instructure.canvas.espresso.common.pages.compose.SettingsPage import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.pages.AssignmentSubmissionListPage -import com.instructure.teacher.ui.pages.ProgressPage +import com.instructure.teacher.ui.pages.compose.AssignmentSubmissionListPage +import com.instructure.teacher.ui.pages.compose.ProgressPage import com.instructure.teacher.ui.pages.compose.SpeedGraderGradePage import com.instructure.teacher.ui.pages.compose.SpeedGraderPage diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt index 4267822a86..b28c3be183 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt @@ -32,53 +32,54 @@ import com.instructure.canvas.espresso.common.pages.LegalPage import com.instructure.canvas.espresso.common.pages.LoginFindSchoolPage import com.instructure.canvas.espresso.common.pages.LoginLandingPage import com.instructure.canvas.espresso.common.pages.LoginSignInPage +import com.instructure.canvas.espresso.common.pages.WrongDomainPage import com.instructure.espresso.InstructureActivityTestRule import com.instructure.espresso.ModuleItemInteractions import com.instructure.espresso.Searchable import com.instructure.teacher.BuildConfig import com.instructure.teacher.R import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.espresso.TestAppManager -import com.instructure.teacher.ui.pages.AnnouncementsListPage -import com.instructure.teacher.ui.pages.AssigneeListPage -import com.instructure.teacher.ui.pages.AssignmentDetailsPage -import com.instructure.teacher.ui.pages.AssignmentDueDatesPage -import com.instructure.teacher.ui.pages.CommentLibraryPage -import com.instructure.teacher.ui.pages.CourseBrowserPage -import com.instructure.teacher.ui.pages.CourseSettingsPage -import com.instructure.teacher.ui.pages.DashboardPage -import com.instructure.teacher.ui.pages.DiscussionsDetailsPage -import com.instructure.teacher.ui.pages.DiscussionsListPage -import com.instructure.teacher.ui.pages.EditAnnouncementDetailsPage -import com.instructure.teacher.ui.pages.EditAssignmentDetailsPage -import com.instructure.teacher.ui.pages.EditDashboardPage -import com.instructure.teacher.ui.pages.EditDiscussionsDetailsPage -import com.instructure.teacher.ui.pages.EditPageDetailsPage -import com.instructure.teacher.ui.pages.EditProfileSettingsPage -import com.instructure.teacher.ui.pages.EditQuizDetailsPage -import com.instructure.teacher.ui.pages.EditSyllabusPage -import com.instructure.teacher.ui.pages.FileListPage -import com.instructure.teacher.ui.pages.HelpPage -import com.instructure.teacher.ui.pages.LeftSideNavigationDrawerPage -import com.instructure.teacher.ui.pages.ModulesPage -import com.instructure.teacher.ui.pages.NavDrawerPage -import com.instructure.teacher.ui.pages.NotATeacherPage -import com.instructure.teacher.ui.pages.PageListPage -import com.instructure.teacher.ui.pages.PeopleListPage -import com.instructure.teacher.ui.pages.PersonContextPage -import com.instructure.teacher.ui.pages.PostSettingsPage -import com.instructure.teacher.ui.pages.ProfileSettingsPage -import com.instructure.teacher.ui.pages.PushNotificationsPage -import com.instructure.teacher.ui.pages.QuizDetailsPage -import com.instructure.teacher.ui.pages.QuizListPage -import com.instructure.teacher.ui.pages.RemoteConfigSettingsPage -import com.instructure.teacher.ui.pages.SpeedGraderCommentsPage -import com.instructure.teacher.ui.pages.SpeedGraderQuizSubmissionPage -import com.instructure.teacher.ui.pages.StudentContextPage -import com.instructure.teacher.ui.pages.SyllabusPage -import com.instructure.teacher.ui.pages.TodoPage -import com.instructure.teacher.ui.pages.UpdateFilePermissionsPage -import com.instructure.teacher.ui.pages.WebViewLoginPage +import com.instructure.teacher.espresso.TestAppManager +import com.instructure.teacher.ui.pages.classic.AnnouncementsListPage +import com.instructure.teacher.ui.pages.classic.AssigneeListPage +import com.instructure.teacher.ui.pages.classic.AssignmentDetailsPage +import com.instructure.teacher.ui.pages.classic.AssignmentDueDatesPage +import com.instructure.teacher.ui.pages.classic.CommentLibraryPage +import com.instructure.teacher.ui.pages.classic.CourseBrowserPage +import com.instructure.teacher.ui.pages.classic.CourseSettingsPage +import com.instructure.teacher.ui.pages.classic.DashboardPage +import com.instructure.teacher.ui.pages.classic.DiscussionsDetailsPage +import com.instructure.teacher.ui.pages.classic.DiscussionsListPage +import com.instructure.teacher.ui.pages.classic.EditAnnouncementDetailsPage +import com.instructure.teacher.ui.pages.classic.EditAssignmentDetailsPage +import com.instructure.teacher.ui.pages.classic.EditDashboardPage +import com.instructure.teacher.ui.pages.classic.EditDiscussionsDetailsPage +import com.instructure.teacher.ui.pages.classic.EditPageDetailsPage +import com.instructure.teacher.ui.pages.classic.EditProfileSettingsPage +import com.instructure.teacher.ui.pages.classic.EditQuizDetailsPage +import com.instructure.teacher.ui.pages.classic.EditSyllabusPage +import com.instructure.teacher.ui.pages.classic.FileListPage +import com.instructure.teacher.ui.pages.classic.HelpPage +import com.instructure.teacher.ui.pages.classic.LeftSideNavigationDrawerPage +import com.instructure.teacher.ui.pages.classic.ModulesPage +import com.instructure.teacher.ui.pages.classic.NavDrawerPage +import com.instructure.teacher.ui.pages.classic.NotATeacherPage +import com.instructure.teacher.ui.pages.classic.PageListPage +import com.instructure.teacher.ui.pages.classic.PeopleListPage +import com.instructure.teacher.ui.pages.classic.PersonContextPage +import com.instructure.teacher.ui.pages.classic.PostSettingsPage +import com.instructure.teacher.ui.pages.classic.ProfileSettingsPage +import com.instructure.teacher.ui.pages.classic.PushNotificationsPage +import com.instructure.teacher.ui.pages.classic.QuizDetailsPage +import com.instructure.teacher.ui.pages.classic.QuizListPage +import com.instructure.teacher.ui.pages.classic.RemoteConfigSettingsPage +import com.instructure.teacher.ui.pages.classic.SpeedGraderCommentsPage +import com.instructure.teacher.ui.pages.classic.SpeedGraderQuizSubmissionPage +import com.instructure.teacher.ui.pages.classic.StudentContextPage +import com.instructure.teacher.ui.pages.classic.SyllabusPage +import com.instructure.teacher.ui.pages.classic.TodoPage +import com.instructure.teacher.ui.pages.classic.UpdateFilePermissionsPage +import com.instructure.teacher.ui.pages.classic.WebViewLoginPage import instructure.rceditor.RCETextEditor import org.hamcrest.Matcher import org.junit.Before @@ -126,6 +127,7 @@ abstract class TeacherTest : CanvasTest() { val loginFindSchoolPage = LoginFindSchoolPage() val loginLandingPage = LoginLandingPage() val loginSignInPage = LoginSignInPage() + val wrongDomainPage = WrongDomainPage() val canvasNetworkSignInPage = CanvasNetworkSignInPage() val moduleListPage = ModulesPage() val navDrawerPage = NavDrawerPage() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/ViewInteractionDelegates.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/ViewInteractionDelegates.kt deleted file mode 100644 index 31b9c2a367..0000000000 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/ViewInteractionDelegates.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2018 - present Instructure, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.instructure.teacher.ui.utils - -import androidx.test.espresso.Espresso -import androidx.test.espresso.ViewInteraction -import androidx.test.espresso.matcher.ViewMatchers -import android.view.View -import com.instructure.espresso.ViewInteractionDelegate -import com.instructure.teacher.R -import org.hamcrest.Matcher -import org.hamcrest.Matchers - -/** - * The toolbar's title text view's resource id is the same as the course text view in course cards. - * Use this to narrow the matcher to the toolbar itself. - */ -class WaitForToolbarTitle(val text: Int, autoAssert: Boolean = true) : ViewInteractionDelegate(autoAssert) { - override fun onProvide(matcher: Matcher): ViewInteraction = Espresso.onView(matcher) - override fun getMatcher(): Matcher { - return Matchers.allOf(ViewMatchers.withText(text), ViewMatchers.withParent(ViewMatchers.withId(R.id.toolbar))) - } -} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/PageExtensions.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/PageInteractionExtensions.kt similarity index 96% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/PageExtensions.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/PageInteractionExtensions.kt index d18f5e620e..1e787286dd 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/PageExtensions.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/PageInteractionExtensions.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.teacher.ui.utils +package com.instructure.teacher.ui.utils.extensions import androidx.test.espresso.action.ViewActions import com.instructure.espresso.click diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTestExtensions.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/TeacherTestExtensions.kt similarity index 99% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTestExtensions.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/TeacherTestExtensions.kt index 4c9e721f98..1e4de4c00e 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTestExtensions.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/TeacherTestExtensions.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.teacher.ui.utils +package com.instructure.teacher.ui.utils.extensions import android.app.Activity import android.content.Intent @@ -60,6 +60,7 @@ import com.instructure.interactions.router.Route import com.instructure.teacher.R import com.instructure.teacher.activities.LoginActivity import com.instructure.teacher.router.RouteMatcher +import com.instructure.teacher.ui.utils.TeacherTest import org.hamcrest.CoreMatchers.allOf import org.hamcrest.Matchers.anyOf import java.io.BufferedInputStream diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/WebInteractionExtensions.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/WebInteractionExtensions.kt similarity index 71% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/WebInteractionExtensions.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/WebInteractionExtensions.kt index f855b406d8..cf334ae429 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/WebInteractionExtensions.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/extensions/WebInteractionExtensions.kt @@ -1,4 +1,20 @@ -package com.instructure.teacher.ui.utils +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.teacher.ui.utils.extensions import androidx.test.espresso.web.model.Atom import androidx.test.espresso.web.model.Evaluation diff --git a/apps/teacher/src/main/AndroidManifest.xml b/apps/teacher/src/main/AndroidManifest.xml index b77408b6ec..3552839f49 100644 --- a/apps/teacher/src/main/AndroidManifest.xml +++ b/apps/teacher/src/main/AndroidManifest.xml @@ -275,6 +275,7 @@ + diff --git a/apps/teacher/src/main/java/com/instructure/teacher/PSPDFKit/AnnotationComments/AnnotationCommentViewHolder.kt b/apps/teacher/src/main/java/com/instructure/teacher/PSPDFKit/AnnotationComments/AnnotationCommentViewHolder.kt index 03c4108c24..c66fcf72b2 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/PSPDFKit/AnnotationComments/AnnotationCommentViewHolder.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/PSPDFKit/AnnotationComments/AnnotationCommentViewHolder.kt @@ -51,7 +51,7 @@ class AnnotationCommentViewHolder(private val binding: AdapterAnnotationCommentB } commentEditIcon.onClick { - val popup = PopupMenu(context, it, Gravity.TOP, 0, com.google.android.material.R.style.Widget_AppCompat_PopupMenu_Overflow) + val popup = PopupMenu(context, it, Gravity.TOP, 0, com.google.android.material.R.style.Widget_Material3_PopupMenu_Overflow) popup.inflate(R.menu.menu_edit_annotation_comment) if(!canEdit) popup.menu.removeItem(R.id.edit) if(!canDelete) popup.menu.removeItem(R.id.delete) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt index 631c9a6181..84ca934b28 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt @@ -77,7 +77,6 @@ import com.instructure.pandautils.features.inbox.list.OnUnreadCountInvalidated import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.features.reminder.AlarmScheduler import com.instructure.pandautils.features.settings.SettingsFragment -import com.instructure.pandautils.features.themeselector.ThemeSelectorBottomSheet import com.instructure.pandautils.interfaces.NavigationCallbacks import com.instructure.pandautils.models.PushNotification import com.instructure.pandautils.receivers.PushExternalReceiver @@ -246,12 +245,6 @@ class InitActivity : BasePresenterActivity by lazy { + @Suppress("UNCHECKED_CAST") + intent.extras?.getSerializable(SELECTED_FILTERS) as? Set + ?: setOf(SubmissionListFilter.ALL) + } + private val filterValueAbove: Double? by lazy { + intent.extras?.getDouble(FILTER_VALUE_ABOVE)?.takeIf { intent.extras!!.containsKey(FILTER_VALUE_ABOVE) } + } + private val filterValueBelow: Double? by lazy { + intent.extras?.getDouble(FILTER_VALUE_BELOW)?.takeIf { intent.extras!!.containsKey(FILTER_VALUE_BELOW) } } - private val filterValue: Double by lazy { intent.extras!!.getDouble(FILTER_VALUE) } private val initialSelection: Int by lazy { intent.extras!!.getInt(Const.SELECTED_ITEM, 0) } private var currentSelection = 0 @@ -159,8 +163,9 @@ class SpeedGraderActivity : BasePresenterActivity = setOf(SubmissionListFilter.ALL), + filterValueAbove: Double? = null, + filterValueBelow: Double? = null ): Bundle { return Bundle().apply { putLong(Const.COURSE_ID, courseId) putLong(Const.ASSIGNMENT_ID, assignmentId) putInt(Const.SELECTED_ITEM, selectedIdx) putBoolean(Const.ANONYMOUS_GRADING, anonymousGrading ?: false) - putSerializable(FILTER, filter) - putDouble(FILTER_VALUE, filterValue) + putSerializable(SELECTED_FILTERS, HashSet(selectedFilters)) + filterValueAbove?.let { putDouble(FILTER_VALUE_ABOVE, it) } + filterValueBelow?.let { putDouble(FILTER_VALUE_BELOW, it) } putLongArray(FILTERED_SUBMISSION_IDS, filteredSubmissionIds) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/di/SyllabusModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/di/SyllabusModule.kt new file mode 100644 index 0000000000..b71ebc2de3 --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/di/SyllabusModule.kt @@ -0,0 +1,22 @@ +package com.instructure.teacher.di + +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.apis.PlannerAPI +import com.instructure.teacher.features.syllabus.SyllabusRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class SyllabusModule { + + @Provides + fun provideSyllabusRepository( + plannerApi: PlannerAPI.PlannerInterface, + calendarEventApi: CalendarEventAPI.CalendarEventInterface + ): SyllabusRepository { + return SyllabusRepository(plannerApi, calendarEventApi) + } +} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/factory/SpeedGraderPresenterFactory.kt b/apps/teacher/src/main/java/com/instructure/teacher/factory/SpeedGraderPresenterFactory.kt index 2b2c768be1..2b0dc3adcf 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/factory/SpeedGraderPresenterFactory.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/factory/SpeedGraderPresenterFactory.kt @@ -30,8 +30,9 @@ class SpeedGraderPresenterFactory( private val discussionEntries: DiscussionTopicHeader?, private val repository: AssignmentSubmissionRepository, private val filteredSubmissionIds: LongArray, - private val filter: SubmissionListFilter, - private val filterValue: Double + private val selectedFilters: Set, + private val filterValueAbove: Double?, + private val filterValueBelow: Double? ) : PresenterFactory { - override fun create() = SpeedGraderPresenter(courseId, assignmentId, submissionId, discussionEntries, repository, filteredSubmissionIds, filter, filterValue) + override fun create() = SpeedGraderPresenter(courseId, assignmentId, submissionId, discussionEntries, repository, filteredSubmissionIds, selectedFilters, filterValueAbove, filterValueBelow) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsFragment.kt index c30dd39ec0..0f6ec0f24c 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsFragment.kt @@ -46,6 +46,7 @@ import com.instructure.pandautils.features.speedgrader.SpeedGraderFragment import com.instructure.pandautils.features.speedgrader.SubmissionListFilter import com.instructure.pandautils.fragments.BasePresenterFragment import com.instructure.pandautils.utils.AssignmentGradedEvent +import com.instructure.pandautils.utils.FeatureFlagProvider import com.instructure.pandautils.utils.LongArg import com.instructure.pandautils.utils.ParcelableArg import com.instructure.pandautils.utils.ThemePrefs @@ -100,6 +101,9 @@ class AssignmentDetailsFragment : BasePresenterFragment< @Inject lateinit var assignmentsApi: AssignmentAPI.AssignmentInterface + @Inject + lateinit var featureFlagProvider: FeatureFlagProvider + private var assignment: Assignment by ParcelableArg(Assignment(), ASSIGNMENT) private var course: Course by ParcelableArg(Course()) private var assignmentId: Long by LongArg(0L, ASSIGNMENT_ID) @@ -331,11 +335,25 @@ class AssignmentDetailsFragment : BasePresenterFragment< } // Load description - loadHtmlJob = descriptionWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), assignment.description, { - descriptionWebViewWrapper.loadHtml(it, assignment.name, baseUrl = assignment.htmlUrl) - }) { - RouteMatcher.route(requireActivity(), LtiLaunchFragment.makeSessionlessLtiUrlRoute(requireActivity(), course, it)) - } + loadHtmlJob = descriptionWebViewWrapper.webView.loadHtmlWithIframes( + requireContext(), + featureFlagProvider, + assignment.description, + { + descriptionWebViewWrapper.loadHtml( + it, + assignment.name, + baseUrl = assignment.htmlUrl + ) + }, + onLtiButtonPressed = { + RouteMatcher.route( + requireActivity(), + LtiLaunchFragment.makeSessionlessLtiUrlRoute(requireActivity(), course, it) + ) + }, + courseId = course.id + ) } private fun configureSubmissionDonuts(assignment: Assignment): Unit = with(binding) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/DifferentiationTag.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/DifferentiationTag.kt new file mode 100644 index 0000000000..9f1e3b95aa --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/DifferentiationTag.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.teacher.features.assignment.submission + +data class DifferentiationTag( + val id: String, + val name: String, + val groupSetName: String?, + val userIds: List +) + +enum class SubmissionSortOrder { + STUDENT_SORTABLE_NAME, + STUDENT_NAME, + SUBMISSION_DATE, + SUBMISSION_STATUS +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListFiltersScreen.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListFiltersScreen.kt index dc4d17e81f..7c7b1034b0 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListFiltersScreen.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListFiltersScreen.kt @@ -18,25 +18,29 @@ package com.instructure.teacher.features.assignment.submission import android.content.res.Configuration import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.OutlinedTextField import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TextButton -import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType @@ -46,35 +50,46 @@ import androidx.compose.ui.unit.sp import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Section import com.instructure.canvasapi2.utils.NumberHelper +import com.instructure.pandautils.compose.composables.BasicTextFieldWithHintDecoration import com.instructure.pandautils.compose.composables.CanvasAppBar import com.instructure.pandautils.compose.composables.CanvasDivider import com.instructure.pandautils.compose.composables.CheckboxText import com.instructure.pandautils.compose.composables.FullScreenDialog import com.instructure.pandautils.compose.composables.RadioButtonText import com.instructure.pandautils.features.speedgrader.SubmissionListFilter +import com.instructure.pandautils.utils.orDefault import com.instructure.teacher.R +data class SubmissionListFiltersUiState( + val selectedFilters: Set, + val filterValueAbove: Double?, + val filterValueBelow: Double?, + val assignmentMaxPoints: Double?, + val courseColor: Color, + val assignmentName: String, + val anonymousGrading: Boolean, + val sections: List, + val initialSelectedSections: List, + val differentiationTags: List, + val selectedDifferentiationTagIds: Set, + val includeStudentsWithoutTags: Boolean, + val sortOrder: SubmissionSortOrder, + val customGradeStatuses: List, + val selectedCustomStatusIds: Set, + val actionHandler: (SubmissionListAction) -> Unit +) + @Composable fun SubmissionListFilters( - filter: SubmissionListFilter, - filterValue: Double?, - courseColor: Color, - assignmentName: String, - sections: List, - initialSelectedSections: List, - actionHandler: (SubmissionListAction) -> Unit, + uiState: SubmissionListFiltersUiState, + modifier: Modifier = Modifier, dismiss: () -> Unit ) { FullScreenDialog(onDismissRequest = { dismiss() }) { SubmissionFilterScreenContent( - filter = filter, - filterValue = filterValue, - courseColor = courseColor, - assignmentName = assignmentName, - sections = sections, - initialSelectedSections = initialSelectedSections, - actionHandler = actionHandler, + uiState = uiState, + modifier = modifier, dismiss = dismiss ) } @@ -82,60 +97,56 @@ fun SubmissionListFilters( @Composable private fun SubmissionFilterScreenContent( - filter: SubmissionListFilter, - filterValue: Double?, - courseColor: Color, - assignmentName: String, - sections: List, - initialSelectedSections: List, - actionHandler: (SubmissionListAction) -> Unit, + uiState: SubmissionListFiltersUiState, + modifier: Modifier = Modifier, dismiss: () -> Unit ) { - var selectedFilter by remember { mutableStateOf(filter) } - var selectedFilterValue by remember { - mutableStateOf(filterValue?.let { - NumberHelper.formatDecimal( - it, - 2, - true - ) + var filters by rememberSaveable { mutableStateOf(uiState.selectedFilters) } + var valueAbove by rememberSaveable { + mutableStateOf(uiState.filterValueAbove?.let { + NumberHelper.formatDecimal(it, 2, true) }.orEmpty()) } - val selectedSections by remember { mutableStateOf(initialSelectedSections.toMutableSet()) } - var error by remember { mutableStateOf(false) } + var valueBelow by rememberSaveable { + mutableStateOf(uiState.filterValueBelow?.let { + NumberHelper.formatDecimal(it, 2, true) + }.orEmpty()) + } + var selectedSections by rememberSaveable { mutableStateOf(uiState.initialSelectedSections.toSet()) } + var selectedTags by rememberSaveable { mutableStateOf(uiState.selectedDifferentiationTagIds) } + var includeWithoutTags by rememberSaveable { mutableStateOf(uiState.includeStudentsWithoutTags) } + var selectedSortOrder by rememberSaveable { mutableStateOf(uiState.sortOrder) } + var selectedCustomStatuses by rememberSaveable { mutableStateOf(uiState.selectedCustomStatusIds) } Scaffold( backgroundColor = colorResource(id = R.color.backgroundLightest), topBar = { CanvasAppBar( title = stringResource(R.string.preferences), - subtitle = assignmentName, + subtitle = uiState.assignmentName, navigationActionClick = { dismiss() }, navIconContentDescription = stringResource(R.string.close), navIconRes = R.drawable.ic_close, - backgroundColor = courseColor, + backgroundColor = uiState.courseColor, textColor = colorResource(id = R.color.textLightest), actions = { TextButton( modifier = Modifier.testTag("appBarDoneButton"), onClick = { - if (selectedFilter in listOf( - SubmissionListFilter.BELOW_VALUE, - SubmissionListFilter.ABOVE_VALUE - ) && selectedFilterValue.isEmpty() - ) { - error = true - } else { - actionHandler( - SubmissionListAction.SetFilters( - selectedFilter, - selectedFilterValue.toDoubleOrNull(), - selectedSections.toList() - ) + uiState.actionHandler( + SubmissionListAction.SetFilters( + selectedFilters = filters, + filterValueAbove = valueAbove.toDoubleOrNull(), + filterValueBelow = valueBelow.toDoubleOrNull(), + selectedSections = selectedSections.toList(), + selectedDifferentiationTagIds = selectedTags, + includeStudentsWithoutTags = includeWithoutTags, + sortOrder = selectedSortOrder, + selectedCustomStatusIds = selectedCustomStatuses ) - dismiss() - } + ) + dismiss() }) { Text( text = stringResource(R.string.done), @@ -148,137 +159,226 @@ private fun SubmissionFilterScreenContent( }) { padding -> Column( modifier = Modifier - .padding(padding) .verticalScroll(rememberScrollState()) ) { - Header(text = stringResource(R.string.submissionFilter)) - FilterItem( - text = stringResource(R.string.all_submissions), - selected = selectedFilter == SubmissionListFilter.ALL, - courseColor = courseColor, - onClick = { - selectedFilter = SubmissionListFilter.ALL - } - ) - FilterItem( - text = stringResource(R.string.submitted_late), - selected = selectedFilter == SubmissionListFilter.LATE, - courseColor = courseColor, - onClick = { - selectedFilter = SubmissionListFilter.LATE - } - ) - FilterItem( - text = stringResource(R.string.needsGrading), - selected = selectedFilter == SubmissionListFilter.NOT_GRADED, - courseColor = courseColor, - onClick = { - selectedFilter = SubmissionListFilter.NOT_GRADED - } - ) - FilterItem( - text = stringResource(R.string.not_submitted), - selected = selectedFilter == SubmissionListFilter.MISSING, - courseColor = courseColor, - onClick = { - selectedFilter = SubmissionListFilter.MISSING - } - ) - FilterItem( - text = stringResource(R.string.graded), - selected = selectedFilter == SubmissionListFilter.GRADED, - courseColor = courseColor, - onClick = { - selectedFilter = SubmissionListFilter.GRADED - } - ) - FilterItem( - text = stringResource(R.string.scored_less_than), - selected = selectedFilter == SubmissionListFilter.BELOW_VALUE, - courseColor = courseColor, - onClick = { - selectedFilterValue = "" - selectedFilter = SubmissionListFilter.BELOW_VALUE - } - ) - if (selectedFilter == SubmissionListFilter.BELOW_VALUE) { - OutlinedTextField( + Header(text = stringResource(R.string.statuses)) + SubmissionListFilter.entries.filter { + listOf( + SubmissionListFilter.ALL, + SubmissionListFilter.ABOVE_VALUE, + SubmissionListFilter.BELOW_VALUE + ).contains(it).not() + }.forEach { filter -> + CheckboxText( + text = when (filter) { + SubmissionListFilter.LATE -> stringResource(R.string.late) + SubmissionListFilter.MISSING -> stringResource(R.string.missingTag) + SubmissionListFilter.NOT_GRADED -> stringResource(R.string.needsGrading) + SubmissionListFilter.GRADED -> stringResource(R.string.graded) + SubmissionListFilter.SUBMITTED -> stringResource(R.string.submitted) + else -> "" + }, + testTag = "statusCheckBox", + selected = filters.contains(filter), + color = uiState.courseColor, + onCheckedChanged = { + filters = if (filters.contains(filter)) { + filters - filter + } else { + (filters - SubmissionListFilter.ALL) + filter + }.let { it.ifEmpty { setOf(SubmissionListFilter.ALL) } } + }, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 8.dp), - value = selectedFilterValue, - onValueChange = { - error = false - selectedFilterValue = it + .defaultMinSize(minHeight = 56.dp) + .padding(horizontal = 4.dp) + ) + } + + // Custom grade statuses + uiState.customGradeStatuses.forEach { status -> + CheckboxText( + text = status.name, + testTag = "customStatusCheckBox", + selected = selectedCustomStatuses.contains(status.id), + color = uiState.courseColor, + onCheckedChanged = { + selectedCustomStatuses = if (selectedCustomStatuses.contains(status.id)) { + selectedCustomStatuses - status.id + } else { + selectedCustomStatuses + status.id + } }, - label = { Text(stringResource(R.string.score)) }, - colors = TextFieldDefaults.outlinedTextFieldColors( - textColor = colorResource(R.color.textDarkest), - focusedBorderColor = colorResource(R.color.textDarkest), - unfocusedBorderColor = colorResource(R.color.textDark), - focusedLabelColor = colorResource(R.color.textDarkest), - unfocusedLabelColor = colorResource(R.color.textDark), - cursorColor = colorResource(R.color.textDarkest), - ), + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 56.dp) + .padding(horizontal = 4.dp) + ) + } + + Header(text = stringResource(R.string.precise_filtering)) + Row( + modifier = Modifier + .defaultMinSize(minHeight = 56.dp) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp) + .testTag("preciseFilterAboveRow"), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.scored_more_than), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 21.sp, + color = colorResource(R.color.textDarkest) + ) + Spacer(modifier = Modifier.weight(1f)) + BasicTextFieldWithHintDecoration( + modifier = Modifier + .padding(end = 4.dp) + .testTag("scoreMoreThanField"), + value = valueAbove, + onValueChange = { valueAbove = it }, + hint = stringResource(R.string.write_score_here), + textColor = uiState.courseColor, + hintColor = colorResource(R.color.textDark), keyboardOptions = KeyboardOptions.Default.copy( keyboardType = KeyboardType.Number + ) + ) + Text( + text = pluralStringResource( + R.plurals.pointsPossible, + uiState.assignmentMaxPoints?.toInt().orDefault(), + uiState.assignmentMaxPoints.orDefault() ), - isError = error + fontSize = 16.sp, + color = colorResource(R.color.textDark) ) } - FilterItem( - text = stringResource(R.string.scored_more_than), - selected = selectedFilter == SubmissionListFilter.ABOVE_VALUE, - courseColor = courseColor, - onClick = { - selectedFilterValue = "" - selectedFilter = SubmissionListFilter.ABOVE_VALUE - } - ) - if (selectedFilter == SubmissionListFilter.ABOVE_VALUE) { - OutlinedTextField( + Row( + modifier = Modifier + .defaultMinSize(minHeight = 56.dp) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp) + .testTag("preciseFilterBelowRow"), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.scored_less_than), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 21.sp, + color = colorResource(R.color.textDarkest) + ) + Spacer(modifier = Modifier.weight(1f)) + BasicTextFieldWithHintDecoration( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 8.dp), - value = selectedFilterValue, - onValueChange = { - error = false - selectedFilterValue = it - }, - label = { Text(stringResource(R.string.score)) }, - colors = TextFieldDefaults.outlinedTextFieldColors( - textColor = colorResource(R.color.textDarkest), - focusedBorderColor = colorResource(R.color.textDarkest), - unfocusedBorderColor = colorResource(R.color.textDark), - focusedLabelColor = colorResource(R.color.textDarkest), - unfocusedLabelColor = colorResource(R.color.textDark) - ), + .padding(end = 4.dp) + .testTag("scoreLessThanField"), + value = valueBelow, + onValueChange = { valueBelow = it }, + hint = stringResource(R.string.write_score_here), + textColor = uiState.courseColor, + hintColor = colorResource(R.color.textDark), keyboardOptions = KeyboardOptions.Default.copy( keyboardType = KeyboardType.Number + ) + ) + Text( + text = pluralStringResource( + R.plurals.pointsPossible, + uiState.assignmentMaxPoints?.toInt().orDefault(), + uiState.assignmentMaxPoints.orDefault() ), - isError = error + fontSize = 16.sp, + color = colorResource(R.color.textDark) ) } - if (sections.isNotEmpty()) { - Header(text = stringResource(R.string.filterBySection)) - sections.forEach { section -> + if (uiState.sections.isNotEmpty()) { + Header(text = stringResource(R.string.filterBySection)) + uiState.sections.forEach { section -> CheckboxText( text = section.name.orEmpty(), testTag = "sectionCheckBox", selected = selectedSections.contains(section.id), - color = courseColor, + color = uiState.courseColor, onCheckedChanged = { - if (selectedSections.contains(section.id)) { - selectedSections.remove(section.id) + selectedSections = if (selectedSections.contains(section.id)) { + selectedSections - section.id } else { - selectedSections.add(section.id) + selectedSections + section.id } }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 56.dp) + .padding(horizontal = 4.dp) ) } } + + if (uiState.differentiationTags.isNotEmpty()) { + Header(text = stringResource(R.string.differentiation_tags)) + CheckboxText( + text = stringResource(R.string.students_without_differentiation_tags), + testTag = "includeWithoutTagsCheckBox", + selected = includeWithoutTags, + color = uiState.courseColor, + onCheckedChanged = { includeWithoutTags = it }, + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 56.dp) + .padding(horizontal = 4.dp) + ) + uiState.differentiationTags.forEach { tag -> + CheckboxText( + text = tag.name, + subtitle = tag.groupSetName, + testTag = "differentiationTagCheckBox", + selected = selectedTags.contains(tag.id), + color = uiState.courseColor, + onCheckedChanged = { + selectedTags = if (selectedTags.contains(tag.id)) { + selectedTags - tag.id + } else { + selectedTags + tag.id + } + }, + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 56.dp) + .padding(horizontal = 4.dp) + ) + } + } + + Header(text = stringResource(R.string.sort_by)) + SubmissionSortOrder.entries.filter { + if (uiState.anonymousGrading) { + it != SubmissionSortOrder.STUDENT_NAME && it != SubmissionSortOrder.STUDENT_SORTABLE_NAME + } else { + true + } + }.forEach { order -> + RadioButtonText( + modifier = Modifier + .defaultMinSize(minHeight = 56.dp) + .padding(horizontal = 4.dp) + .fillMaxWidth() + .testTag("${order.name}SortButton"), + text = when (order) { + SubmissionSortOrder.STUDENT_NAME -> stringResource(R.string.student_name) + SubmissionSortOrder.SUBMISSION_DATE -> stringResource(R.string.submission_date) + SubmissionSortOrder.STUDENT_SORTABLE_NAME -> stringResource(R.string.student_sortable_name) + SubmissionSortOrder.SUBMISSION_STATUS -> stringResource(R.string.submission_status) + }, + selected = selectedSortOrder == order, + color = uiState.courseColor, + onClick = { selectedSortOrder = order } + ) + } } } } @@ -296,53 +396,29 @@ private fun Header(text: String) { CanvasDivider() } -@Composable -private fun FilterItem( - text: String, - selected: Boolean, - courseColor: Color, - onClick: () -> Unit -) { - RadioButtonText( - modifier = Modifier - .padding(vertical = 8.dp, horizontal = 16.dp) - .fillMaxWidth() - .testTag("filterItem"), - text = text, - selected = selected, - color = courseColor, - onClick = { - onClick() - } - ) -} - @Preview -@Composable -private fun SubmissionFilterScreenPreview() { - SubmissionFilterScreenContent( - filter = SubmissionListFilter.ALL, - filterValue = null, - courseColor = Color.Black, - assignmentName = "Assignment Name", - sections = listOf(Section(id = 1, name = "Section 1"), Section(id = 2, name = "Section 2")), - initialSelectedSections = listOf(1L), - actionHandler = {}, - dismiss = {} - ) -} - @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true) @Composable -private fun SubmissionFilterScreenDarkPreview() { +private fun SubmissionFilterScreenPreview() { SubmissionFilterScreenContent( - filter = SubmissionListFilter.ABOVE_VALUE, - filterValue = 12.0, - courseColor = Color.Blue, - assignmentName = "Assignment Name", - sections = listOf(Section(id = 1, name = "Section 1"), Section(id = 2, name = "Section 2")), - initialSelectedSections = listOf(2L), - actionHandler = {}, + uiState = SubmissionListFiltersUiState( + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = null, + filterValueBelow = null, + assignmentMaxPoints = 100.0, + courseColor = Color.Black, + assignmentName = "Assignment Name", + anonymousGrading = false, + sections = listOf(Section(id = 1, name = "Section 1"), Section(id = 2, name = "Section 2")), + initialSelectedSections = listOf(1L), + differentiationTags = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + customGradeStatuses = emptyList(), + selectedCustomStatusIds = emptySet(), + actionHandler = {} + ), dismiss = {} ) } \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListFragment.kt index d6de2ce53e..822646c748 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListFragment.kt @@ -105,8 +105,9 @@ class SubmissionListFragment : BaseCanvasFragment() { action.selectedIdx, action.anonymousGrading, action.filteredSubmissionIds, - action.filter, - action.filterValue + action.selectedFilters, + action.filterValueAbove, + action.filterValueBelow ) RouteMatcher.route(requireActivity(), Route(bundle, RouteContext.SPEED_GRADER)) } @@ -145,25 +146,25 @@ class SubmissionListFragment : BaseCanvasFragment() { @Suppress("unused") @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onAssignmentGraded(event: AssignmentGradedEvent) { - viewModel.uiState.value.actionHandler(SubmissionListAction.Refresh) + viewModel.uiState.value.filtersUiState.actionHandler(SubmissionListAction.Refresh) } @Suppress("unused") @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onQuizGraded(event: QuizSubmissionGradedEvent) { - viewModel.uiState.value.actionHandler(SubmissionListAction.Refresh) + viewModel.uiState.value.filtersUiState.actionHandler(SubmissionListAction.Refresh) } @Suppress("unused") @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onSubmissionCommentUpdated(event: SubmissionCommentsUpdated) { - viewModel.uiState.value.actionHandler(SubmissionListAction.Refresh) + viewModel.uiState.value.filtersUiState.actionHandler(SubmissionListAction.Refresh) } @Suppress("unused") @Subscribe(threadMode = ThreadMode.MAIN) fun onSubmissionFilterChanged(event: SubmissionFilterChangedEvent) { - viewModel.uiState.value.actionHandler(SubmissionListAction.Refresh) + viewModel.uiState.value.filtersUiState.actionHandler(SubmissionListAction.Refresh) } companion object { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListScreen.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListScreen.kt index 69ea505ad8..725aaef54a 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListScreen.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListScreen.kt @@ -17,7 +17,6 @@ package com.instructure.teacher.features.assignment.submission import android.content.res.Configuration -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -44,6 +43,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -52,6 +52,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -68,18 +69,18 @@ import com.instructure.teacher.R @Composable fun SubmissionListScreen(uiState: SubmissionListUiState, navigationIconClick: () -> Unit) { - var showFilterDialog by remember { mutableStateOf(false) } + var showFilterDialog by rememberSaveable { mutableStateOf(false) } Scaffold( backgroundColor = colorResource(id = R.color.backgroundLightest), topBar = { CanvasAppBar( title = stringResource(R.string.submissions), - subtitle = uiState.assignmentName, + subtitle = uiState.filtersUiState.assignmentName, navigationActionClick = { navigationIconClick() }, navIconContentDescription = stringResource(R.string.back), navIconRes = R.drawable.ic_back_arrow, - backgroundColor = uiState.courseColor, + backgroundColor = uiState.filtersUiState.courseColor, textColor = colorResource(id = R.color.textLightest), actions = { IconButton( @@ -87,22 +88,30 @@ fun SubmissionListScreen(uiState: SubmissionListUiState, navigationIconClick: () onClick = { showFilterDialog = true }) { + val hasActiveFilters = uiState.filtersUiState.selectedFilters != setOf(SubmissionListFilter.ALL) || + uiState.filtersUiState.initialSelectedSections.isNotEmpty() || + uiState.filtersUiState.selectedDifferentiationTagIds.isNotEmpty() || + uiState.filtersUiState.selectedCustomStatusIds.isNotEmpty() Icon( painter = painterResource( - id = if (uiState.filter != SubmissionListFilter.ALL || uiState.selectedSections.isNotEmpty()) { + id = if (hasActiveFilters) { R.drawable.ic_filter_filled } else { R.drawable.ic_filter_outline } ), - contentDescription = stringResource(R.string.a11y_contentDescription_filter), + contentDescription = if (hasActiveFilters) { + stringResource(R.string.a11y_filter_active) + } else { + stringResource(R.string.a11y_filter_inactive) + }, tint = colorResource(id = R.color.textLightest) ) } IconButton( modifier = Modifier.testTag("postPolicyButton"), - onClick = { uiState.actionHandler(SubmissionListAction.ShowPostPolicy) }) { + onClick = { uiState.filtersUiState.actionHandler(SubmissionListAction.ShowPostPolicy) }) { Icon( painter = painterResource(id = R.drawable.ic_eye), contentDescription = stringResource(R.string.a11y_contentDescription_postPolicy), @@ -112,7 +121,7 @@ fun SubmissionListScreen(uiState: SubmissionListUiState, navigationIconClick: () IconButton( modifier = Modifier.testTag("addMessageButton"), - onClick = { uiState.actionHandler(SubmissionListAction.SendMessage) }) { + onClick = { uiState.filtersUiState.actionHandler(SubmissionListAction.SendMessage) }) { Icon( painter = painterResource(id = R.drawable.ic_mail), contentDescription = stringResource(R.string.a11y_sendMessage), @@ -125,13 +134,7 @@ fun SubmissionListScreen(uiState: SubmissionListUiState, navigationIconClick: () ) { padding -> if (showFilterDialog) { SubmissionListFilters( - uiState.filter, - uiState.filterValue, - uiState.courseColor, - uiState.assignmentName, - uiState.sections, - uiState.selectedSections, - uiState.actionHandler + uiState = uiState.filtersUiState ) { showFilterDialog = false } @@ -145,7 +148,7 @@ fun SubmissionListScreen(uiState: SubmissionListUiState, navigationIconClick: () uiState.error -> { ErrorContent( errorMessage = stringResource(R.string.errorLoadingSubmission), - retryClick = { uiState.actionHandler(SubmissionListAction.Refresh) }, + retryClick = { uiState.filtersUiState.actionHandler(SubmissionListAction.Refresh) }, modifier = Modifier.fillMaxSize() ) } @@ -159,7 +162,7 @@ fun SubmissionListScreen(uiState: SubmissionListUiState, navigationIconClick: () } else -> { - SubmissionListContent(uiState, Modifier.padding(padding), uiState.courseColor) + SubmissionListContent(uiState, Modifier.padding(padding), uiState.filtersUiState.courseColor) } } } @@ -173,7 +176,7 @@ private fun SubmissionListContent( courseColor: Color ) { val pullRefreshState = rememberPullRefreshState(refreshing = uiState.refreshing, onRefresh = { - uiState.actionHandler(SubmissionListAction.Refresh) + uiState.filtersUiState.actionHandler(SubmissionListAction.Refresh) }) Box( modifier = Modifier @@ -181,7 +184,7 @@ private fun SubmissionListContent( .pullRefresh(pullRefreshState) ) { LazyColumn(modifier = modifier.testTag("submissionList")) { - if (!uiState.anonymousGrading) { + if (!uiState.filtersUiState.anonymousGrading) { item { SearchBar( icon = R.drawable.ic_search_white_24dp, @@ -190,26 +193,22 @@ private fun SubmissionListContent( placeholder = stringResource(R.string.search), collapsable = false, onSearch = { - uiState.actionHandler(SubmissionListAction.Search(it)) + uiState.filtersUiState.actionHandler(SubmissionListAction.Search(it)) }, onClear = { - uiState.actionHandler(SubmissionListAction.Search("")) + uiState.filtersUiState.actionHandler(SubmissionListAction.Search("")) }) } } - item { - Header(uiState.headerTitle) - CanvasDivider() - } items(uiState.submissions, key = { it.submissionId }) { submission -> SubmissionListItem(submission, courseColor, - uiState.anonymousGrading, + uiState.filtersUiState.anonymousGrading, avatarClick = { - uiState.actionHandler(SubmissionListAction.AvatarClicked(it)) + uiState.filtersUiState.actionHandler(SubmissionListAction.AvatarClicked(it)) }, itemClick = { - uiState.actionHandler(SubmissionListAction.SubmissionClicked(it)) + uiState.filtersUiState.actionHandler(SubmissionListAction.SubmissionClicked(it)) }) CanvasDivider() } @@ -221,24 +220,7 @@ private fun SubmissionListContent( modifier = Modifier .align(Alignment.TopCenter) .testTag("pullRefreshIndicator"), - contentColor = uiState.courseColor - ) - } -} - -@Composable -private fun Header(title: String) { - Column( - modifier = Modifier - .fillMaxWidth() - .background(colorResource(R.color.backgroundLight)) - ) { - Text( - text = title, - fontSize = 16.sp, - color = colorResource(id = R.color.textDarkest), - fontWeight = FontWeight.SemiBold, - modifier = Modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp, bottom = 14.dp) + contentColor = courseColor ) } } @@ -319,7 +301,10 @@ private fun SubmissionListItem( @Composable private fun SubmissionTag(tag: SubmissionTag, hasDivider: Boolean) { - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.semantics(mergeDescendants = true) {} + ) { tag.icon?.let { Icon( painter = painterResource(id = it), @@ -362,11 +347,25 @@ private fun SubmissionTag(tag: SubmissionTag, hasDivider: Boolean) { fun SubmissionListScreenPreview() { SubmissionListScreen( SubmissionListUiState( - "Test assignment", - courseColor = Color.Magenta, headerTitle = "All Submissions", - filter = SubmissionListFilter.GRADED, - anonymousGrading = false, + filtersUiState = SubmissionListFiltersUiState( + assignmentName = "Test assignment", + courseColor = Color.Magenta, + anonymousGrading = false, + selectedFilters = setOf(SubmissionListFilter.GRADED), + filterValueAbove = null, + filterValueBelow = null, + assignmentMaxPoints = 100.0, + sections = emptyList(), + initialSelectedSections = emptyList(), + differentiationTags = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + customGradeStatuses = emptyList(), + selectedCustomStatusIds = emptySet(), + actionHandler = {} + ), submissions = listOf( SubmissionUiState( 1, @@ -432,7 +431,7 @@ fun SubmissionListScreenPreview() { hidden = false ) ) - ) {} + ) ) {} } @@ -441,11 +440,25 @@ fun SubmissionListScreenPreview() { fun SubmissionListScreenDarkPreview() { SubmissionListScreen( SubmissionListUiState( - "Test assignment", - courseColor = Color.Magenta, - anonymousGrading = false, headerTitle = "All Submissions", - filter = SubmissionListFilter.GRADED, + filtersUiState = SubmissionListFiltersUiState( + assignmentName = "Test assignment", + courseColor = Color.Magenta, + anonymousGrading = false, + selectedFilters = setOf(SubmissionListFilter.GRADED), + filterValueAbove = null, + filterValueBelow = null, + assignmentMaxPoints = 100.0, + sections = emptyList(), + initialSelectedSections = emptyList(), + differentiationTags = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + customGradeStatuses = emptyList(), + selectedCustomStatusIds = emptySet(), + actionHandler = {} + ), submissions = listOf( SubmissionUiState( 1, @@ -518,7 +531,7 @@ fun SubmissionListScreenDarkPreview() { hidden = false ) ) - ) {} + ) ) {} } @@ -527,13 +540,27 @@ fun SubmissionListScreenDarkPreview() { private fun SubmissionListErrorPreview() { SubmissionListScreen( SubmissionListUiState( - "Test assignment", - courseColor = Color.Magenta, - anonymousGrading = false, headerTitle = "All Submissions", - filter = SubmissionListFilter.GRADED, + filtersUiState = SubmissionListFiltersUiState( + assignmentName = "Test assignment", + courseColor = Color.Magenta, + anonymousGrading = false, + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = null, + filterValueBelow = null, + assignmentMaxPoints = 100.0, + sections = emptyList(), + initialSelectedSections = emptyList(), + differentiationTags = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + customGradeStatuses = emptyList(), + selectedCustomStatusIds = emptySet(), + actionHandler = {} + ), error = true - ) {} + ) ) {} } @@ -542,12 +569,26 @@ private fun SubmissionListErrorPreview() { private fun SubmissionListEmptyPreview() { SubmissionListScreen( SubmissionListUiState( - "Test assignment", - courseColor = Color.Magenta, - anonymousGrading = false, headerTitle = "All Submissions", - filter = SubmissionListFilter.GRADED, + filtersUiState = SubmissionListFiltersUiState( + assignmentName = "Test assignment", + courseColor = Color.Magenta, + anonymousGrading = false, + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = null, + filterValueBelow = null, + assignmentMaxPoints = 100.0, + sections = emptyList(), + initialSelectedSections = emptyList(), + differentiationTags = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + customGradeStatuses = emptyList(), + selectedCustomStatusIds = emptySet(), + actionHandler = {} + ), submissions = emptyList() - ) {} + ) ) {} } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListUiState.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListUiState.kt index 5e3c1955ff..c494e9332e 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListUiState.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListUiState.kt @@ -21,6 +21,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.ui.graphics.Color import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Recipient import com.instructure.canvasapi2.models.Section @@ -28,20 +29,18 @@ import com.instructure.pandautils.features.speedgrader.SubmissionListFilter import com.instructure.teacher.R data class SubmissionListUiState( - val assignmentName: String, - val courseColor: Color, val headerTitle: String, - val anonymousGrading: Boolean, val searchQuery: String = "", - val filter: SubmissionListFilter = SubmissionListFilter.ALL, - val filterValue: Double? = null, - val sections: List
= emptyList(), - val selectedSections: List = emptyList(), + val filtersUiState: SubmissionListFiltersUiState, val submissions: List = emptyList(), val loading: Boolean = false, val refreshing: Boolean = false, - val error: Boolean = false, - val actionHandler: (SubmissionListAction) -> Unit + val error: Boolean = false +) + +data class CustomGradeStatus( + val id: String, + val name: String ) data class SubmissionUiState( @@ -88,9 +87,14 @@ sealed class SubmissionListAction { data class SubmissionClicked(val submissionId: Long) : SubmissionListAction() data class Search(val query: String) : SubmissionListAction() data class SetFilters( - val filter: SubmissionListFilter, - val filterValue: Double?, - val selectedSections: List + val selectedFilters: Set, + val filterValueAbove: Double?, + val filterValueBelow: Double?, + val selectedSections: List, + val selectedDifferentiationTagIds: Set, + val includeStudentsWithoutTags: Boolean, + val sortOrder: SubmissionSortOrder, + val selectedCustomStatusIds: Set ) : SubmissionListAction() data object ShowPostPolicy : SubmissionListAction() @@ -105,8 +109,9 @@ sealed class SubmissionListViewModelAction { val selectedIdx: Int, val anonymousGrading: Boolean? = null, val filteredSubmissionIds: LongArray = longArrayOf(), - val filter: SubmissionListFilter? = null, - val filterValue: Double = 0.0 + val selectedFilters: Set = setOf(SubmissionListFilter.ALL), + val filterValueAbove: Double? = null, + val filterValueBelow: Double? = null ) : SubmissionListViewModelAction() data class ShowPostPolicy(val course: Course, val assignment: Assignment) : diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListViewModel.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListViewModel.kt index 50a4bdcc2e..d6c6a30a47 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListViewModel.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/submission/SubmissionListViewModel.kt @@ -62,25 +62,45 @@ class SubmissionListViewModel @Inject constructor( ?: throw IllegalArgumentException("Assignment must be passed to SubmissionListViewModel") private val course: Course = savedStateHandle[SubmissionListFragment.COURSE] ?: throw IllegalArgumentException("Course must be passed to SubmissionListViewModel") - private var filter: SubmissionListFilter = + private var selectedFilters: Set = setOf( savedStateHandle[SubmissionListFragment.FILTER_TYPE] ?: SubmissionListFilter.ALL - private var filterValue: Double? = 0.0 + ) + private var filterValueAbove: Double? = null + private var filterValueBelow: Double? = null private var searchQuery: String = "" private var submissions: List = emptyList() private var sections: List
= emptyList() private var selectedSectionIds = listOf() private var customStatuses = listOf() + private var differentiationTags: List = emptyList() + private var selectedDifferentiationTagIds: Set = emptySet() + private var includeStudentsWithoutTags: Boolean = false + private var sortOrder: SubmissionSortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME + private var selectedCustomStatusIds: Set = emptySet() private val _uiState = MutableStateFlow( SubmissionListUiState( - assignmentName = assignment.name.orEmpty(), - courseColor = Color(course.color), - anonymousGrading = assignment.anonymousGrading, - filter = filter, - headerTitle = getHeaderTitle(filter, filterValue), + headerTitle = getHeaderTitle(selectedFilters, filterValueAbove, filterValueBelow), searchQuery = "", - actionHandler = this::handleAction + filtersUiState = SubmissionListFiltersUiState( + assignmentName = assignment.name.orEmpty(), + courseColor = Color(course.color), + anonymousGrading = assignment.anonymousGrading, + selectedFilters = selectedFilters, + filterValueAbove = filterValueAbove, + filterValueBelow = filterValueBelow, + assignmentMaxPoints = assignment.pointsPossible, + sections = emptyList(), + initialSelectedSections = emptyList(), + differentiationTags = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = sortOrder, + customGradeStatuses = emptyList(), + selectedCustomStatusIds = emptySet(), + actionHandler = this::handleAction + ) ) ) val uiState: StateFlow @@ -108,67 +128,134 @@ class SubmissionListViewModel @Inject constructor( forceNetwork ) sections = submissionListRepository.getSections(course.id, forceNetwork) - _uiState.update { it.copy(sections = sections) } + + val rawDifferentiationTags = submissionListRepository.getDifferentiationTags( + course.id, + forceNetwork + ) + differentiationTags = rawDifferentiationTags.map { (group, groupSetName) -> + val groupName = group.name.orEmpty() + DifferentiationTag( + id = group._id, + name = groupName, + groupSetName = if (groupSetName != groupName) groupSetName else null, + userIds = group.membersConnection?.nodes?.mapNotNull { it?.user?._id }.orEmpty() + ) + } + + _uiState.update { + it.copy( + filtersUiState = it.filtersUiState.copy( + sections = sections, + differentiationTags = differentiationTags, + customGradeStatuses = customStatuses.map { status -> + CustomGradeStatus( + id = status._id, + name = status.name + ) + } + ) + ) + } filterData() - } catch (_: Exception) { + } catch (e: Exception) { + e.printStackTrace() _uiState.update { it.copy(error = true, loading = false, refreshing = false) } } } private fun filterData() { - val submissionUiStates = submissions.filter { submission -> - when (filter) { - SubmissionListFilter.ALL -> true - SubmissionListFilter.LATE -> submission.submission?.let { - assignment.getState( - it, - true - ) in listOf( - AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED_LATE, - AssignmentUtils2.ASSIGNMENT_STATE_GRADED_LATE - ) - } == true + var filteredSubmissions = submissions - SubmissionListFilter.NOT_GRADED -> submission.submission?.let { - (assignment.getState( - it, - true - ) in listOf( - AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED, - AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED_LATE - ) || !it.isGradeMatchesCurrentSubmission) && submission.submission?.customGradeStatusId == null - } == true + filteredSubmissions = filteredSubmissions.filter { submission -> + if (selectedFilters.contains(SubmissionListFilter.ALL) && selectedCustomStatusIds.isEmpty()) return@filter true + + val activeFilters = if (selectedCustomStatusIds.isNotEmpty()) { + selectedFilters - SubmissionListFilter.ALL + } else { + selectedFilters + } - SubmissionListFilter.GRADED -> submission.submission?.let { - (assignment.getState( - it, - true - ) in listOf( - AssignmentUtils2.ASSIGNMENT_STATE_GRADED, - AssignmentUtils2.ASSIGNMENT_STATE_GRADED_LATE, - AssignmentUtils2.ASSIGNMENT_STATE_GRADED_MISSING, - AssignmentUtils2.ASSIGNMENT_STATE_EXCUSED - ) && it.isGradeMatchesCurrentSubmission) || submission.submission?.customGradeStatusId != null + val matchesStandardFilter = activeFilters.any { filter -> + when (filter) { + SubmissionListFilter.ALL -> true + SubmissionListFilter.LATE -> submission.submission?.let { + assignment.getState(it, true) in listOf( + AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED_LATE, + AssignmentUtils2.ASSIGNMENT_STATE_GRADED_LATE + ) + } == true + + SubmissionListFilter.NOT_GRADED -> submission.submission?.let { + (assignment.getState(it, true) in listOf( + AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED, + AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED_LATE + ) || !it.isGradeMatchesCurrentSubmission) && submission.submission?.customGradeStatusId == null + } == true + + SubmissionListFilter.GRADED -> submission.submission?.let { + (assignment.getState(it, true) in listOf( + AssignmentUtils2.ASSIGNMENT_STATE_GRADED, + AssignmentUtils2.ASSIGNMENT_STATE_GRADED_LATE, + AssignmentUtils2.ASSIGNMENT_STATE_GRADED_MISSING, + AssignmentUtils2.ASSIGNMENT_STATE_EXCUSED + ) && it.isGradeMatchesCurrentSubmission) + } == true + + SubmissionListFilter.MISSING -> submission.submission?.workflowState == "unsubmitted" || submission.submission == null + + SubmissionListFilter.SUBMITTED -> submission.submission?.let { + !it.late && !it.excused && !it.isGraded && it.workflowState != "unsubmitted" + } == true + + SubmissionListFilter.ABOVE_VALUE, SubmissionListFilter.BELOW_VALUE -> false + } + } + + val matchesCustomStatus = if (selectedCustomStatusIds.isNotEmpty()) { + submission.submission?.customGradeStatusId?.let { statusId -> + selectedCustomStatusIds.contains(statusId.toString()) } == true + } else { + false + } - SubmissionListFilter.ABOVE_VALUE -> submission.submission?.let { !it.excused && it.isGraded && it.score >= filterValue.orDefault() } == true + matchesStandardFilter || matchesCustomStatus + } - SubmissionListFilter.BELOW_VALUE -> submission.submission?.let { !it.excused && it.isGraded && it.score < filterValue.orDefault() } == true - // Filtering by ASSIGNMENT_STATE_MISSING here doesn't work because it assumes that the due date has already passed, which isn't necessarily the case when the teacher wants to see - // which students haven't submitted yet - SubmissionListFilter.MISSING -> submission.submission?.workflowState == "unsubmitted" || submission.submission == null + filterValueAbove?.let { aboveValue -> + filteredSubmissions = filteredSubmissions.filter { submission -> + submission.submission?.let { !it.excused && it.isGraded && it.score >= aboveValue } == true } } - .filter { it.assignee.name.contains(searchQuery, true) } - .filter { - if (selectedSectionIds.isEmpty()) return@filter true - (it.assignee as? StudentAssignee)?.student?.enrollments?.any { it.courseSectionId in selectedSectionIds } == true + filterValueBelow?.let { belowValue -> + filteredSubmissions = filteredSubmissions.filter { submission -> + submission.submission?.let { !it.excused && it.isGraded && it.score < belowValue } == true } - .map { getSubmissionUiState(it) } + } - if (assignment.anonymousGrading) { - submissions = submissions.shuffled(Random(1234)) + filteredSubmissions = filteredSubmissions.filter { submission -> + if (selectedSectionIds.isEmpty()) return@filter true + (submission.assignee as? StudentAssignee)?.student?.enrollments?.any { it.courseSectionId in selectedSectionIds } == true + } + + if (selectedDifferentiationTagIds.isNotEmpty() || includeStudentsWithoutTags) { + filteredSubmissions = filteredSubmissions.filter { submission -> + matchesDifferentiationTagFilter(submission, selectedDifferentiationTagIds, includeStudentsWithoutTags) + } + } + + filteredSubmissions = filteredSubmissions.filter { submission -> + submission.assignee.name.contains(searchQuery, true) + } + + val sorted = filteredSubmissions.sortedWith(getSortComparator()) + + val submissionUiStates = if (assignment.anonymousGrading) { + sorted.shuffled(Random(ANONYMOUS_SHUFFLE_SEED)).map { getSubmissionUiState(it) } + } else { + sorted.map { getSubmissionUiState(it) } } _uiState.update { @@ -180,6 +267,67 @@ class SubmissionListViewModel @Inject constructor( } } + private fun matchesDifferentiationTagFilter( + submission: GradeableStudentSubmission, + selectedTags: Set, + includeWithoutTags: Boolean + ): Boolean { + val studentId = (submission.assignee as? StudentAssignee)?.student?.id?.toString() + + val belongsToSelectedTag = if (selectedTags.isNotEmpty()) { + studentId?.let { id -> + differentiationTags.any { tag -> + tag.id in selectedTags && id in tag.userIds + } + } == true + } else { + false + } + + val belongsToAnyTag = studentId?.let { id -> + differentiationTags.any { tag -> id in tag.userIds } + } == true + + return when { + includeWithoutTags && selectedTags.isNotEmpty() -> belongsToSelectedTag || !belongsToAnyTag + includeWithoutTags -> !belongsToAnyTag + else -> belongsToSelectedTag + } + } + + private fun getSortComparator(): Comparator { + return when (sortOrder) { + SubmissionSortOrder.STUDENT_SORTABLE_NAME -> compareBy { + (it.assignee as? StudentAssignee)?.student?.sortableName?.lowercase(java.util.Locale.getDefault()) + ?: it.assignee.name.lowercase(java.util.Locale.getDefault()) + } + + SubmissionSortOrder.STUDENT_NAME -> compareBy { + (it.assignee as? StudentAssignee)?.student?.name?.lowercase(java.util.Locale.getDefault()) + ?: it.assignee.name.lowercase(java.util.Locale.getDefault()) + } + + SubmissionSortOrder.SUBMISSION_DATE -> compareByDescending { + it.submission?.submittedAt?.time ?: 0L + }.thenBy { + (it.assignee as? StudentAssignee)?.student?.sortableName?.lowercase(java.util.Locale.getDefault()) + ?: it.assignee.name.lowercase(java.util.Locale.getDefault()) + } + + SubmissionSortOrder.SUBMISSION_STATUS -> compareBy { + when { + it.submission == null || it.submission?.workflowState == "unsubmitted" -> 3 + it.submission?.missing == true -> 2 + it.submission?.late == true -> 1 + else -> 0 + } + }.thenBy { + (it.assignee as? StudentAssignee)?.student?.sortableName?.lowercase(java.util.Locale.getDefault()) + ?: it.assignee.name.lowercase(java.util.Locale.getDefault()) + } + } + } + private fun getSubmissionUiState(submission: GradeableStudentSubmission): SubmissionUiState { return SubmissionUiState( submissionId = submission.id, @@ -264,9 +412,9 @@ class SubmissionListViewModel @Inject constructor( } } .toLongArray(), - filter = filter, - filterValue = filterValue.orDefault( - ) + selectedFilters = selectedFilters, + filterValueAbove = filterValueAbove, + filterValueBelow = filterValueBelow ) ) } @@ -279,15 +427,27 @@ class SubmissionListViewModel @Inject constructor( } is SubmissionListAction.SetFilters -> { - filter = action.filter - filterValue = action.filterValue + selectedFilters = action.selectedFilters + filterValueAbove = action.filterValueAbove + filterValueBelow = action.filterValueBelow selectedSectionIds = action.selectedSections + selectedDifferentiationTagIds = action.selectedDifferentiationTagIds + includeStudentsWithoutTags = action.includeStudentsWithoutTags + sortOrder = action.sortOrder + selectedCustomStatusIds = action.selectedCustomStatusIds _uiState.update { it.copy( - filter = action.filter, - filterValue = action.filterValue, - selectedSections = action.selectedSections, - headerTitle = getHeaderTitle(action.filter, action.filterValue) + headerTitle = getHeaderTitle(action.selectedFilters, action.filterValueAbove, action.filterValueBelow), + filtersUiState = it.filtersUiState.copy( + selectedFilters = action.selectedFilters, + filterValueAbove = action.filterValueAbove, + filterValueBelow = action.filterValueBelow, + initialSelectedSections = action.selectedSections, + selectedDifferentiationTagIds = action.selectedDifferentiationTagIds, + includeStudentsWithoutTags = action.includeStudentsWithoutTags, + sortOrder = action.sortOrder, + selectedCustomStatusIds = action.selectedCustomStatusIds + ) ) } filterData() @@ -345,17 +505,14 @@ class SubmissionListViewModel @Inject constructor( else -> { try { if (assignment.gradingType == Assignment.PERCENT_TYPE) { - val value: Double = - submission.grade?.removeSuffix("%")?.toDouble() as Double + val value = submission.grade?.removeSuffix("%")?.toDouble() ?: 0.0 NumberHelper.doubleToPercentage(value, 2) } else { - NumberHelper.formatDecimal( - submission.grade?.toDouble() as Double, - 2, - true - ) + val gradeValue = submission.grade?.toDouble() ?: 0.0 + NumberHelper.formatDecimal(gradeValue, 2, true) } - } catch (_: Exception) { + } catch (e: Exception) { + e.printStackTrace() submission.grade ?: "-" } } @@ -366,29 +523,69 @@ class SubmissionListViewModel @Inject constructor( } } - private fun getHeaderTitle(filter: SubmissionListFilter, filterValue: Double?): String { - return when (filter) { - SubmissionListFilter.ALL -> { - resources.getString(R.string.all_submissions) - } + private fun getHeaderTitle( + filters: Set, + filterValueAbove: Double?, + filterValueBelow: Double? + ): String { + val hasScoreFilters = filterValueAbove != null || filterValueBelow != null + val hasStatusFilters = !filters.contains(SubmissionListFilter.ALL) && filters.isNotEmpty() - SubmissionListFilter.LATE -> resources.getString(R.string.submitted_late) - SubmissionListFilter.MISSING -> resources.getString(R.string.havent_submitted_yet) - SubmissionListFilter.NOT_GRADED -> resources.getString(R.string.havent_been_graded) - SubmissionListFilter.GRADED -> resources.getString(R.string.graded) - SubmissionListFilter.BELOW_VALUE -> { + val scoreText = when { + filterValueAbove != null && filterValueBelow != null -> { resources.getString( - R.string.scored_less_than_value, - NumberHelper.formatDecimal(filterValue.orDefault(), 2, true) + R.string.scored_between_values, + NumberHelper.formatDecimal(filterValueAbove, 2, true), + NumberHelper.formatDecimal(filterValueBelow, 2, true) ) } - - SubmissionListFilter.ABOVE_VALUE -> { + filterValueAbove != null -> { resources.getString( R.string.scored_more_than_value, - NumberHelper.formatDecimal(filterValue.orDefault(), 2, true) + NumberHelper.formatDecimal(filterValueAbove, 2, true) + ) + } + filterValueBelow != null -> { + resources.getString( + R.string.scored_less_than_value, + NumberHelper.formatDecimal(filterValueBelow, 2, true) ) } + else -> "" + } + + return when { + hasScoreFilters && !hasStatusFilters -> scoreText + hasScoreFilters && hasStatusFilters -> { + val statusText = if (filters.size > 1) { + resources.getString(R.string.multiple_filters) + } else { + when (filters.first()) { + SubmissionListFilter.LATE -> resources.getString(R.string.submitted_late) + SubmissionListFilter.MISSING -> resources.getString(R.string.havent_submitted_yet) + SubmissionListFilter.NOT_GRADED -> resources.getString(R.string.havent_been_graded) + SubmissionListFilter.GRADED -> resources.getString(R.string.graded) + SubmissionListFilter.SUBMITTED -> resources.getString(R.string.submitted) + else -> resources.getString(R.string.all_submissions) + } + } + "$statusText â€ĸ $scoreText" + } + !hasScoreFilters && hasStatusFilters -> { + if (filters.size > 1) { + resources.getString(R.string.multiple_filters) + } else { + when (filters.first()) { + SubmissionListFilter.LATE -> resources.getString(R.string.submitted_late) + SubmissionListFilter.MISSING -> resources.getString(R.string.havent_submitted_yet) + SubmissionListFilter.NOT_GRADED -> resources.getString(R.string.havent_been_graded) + SubmissionListFilter.GRADED -> resources.getString(R.string.graded) + SubmissionListFilter.SUBMITTED -> resources.getString(R.string.submitted) + else -> resources.getString(R.string.all_submissions) + } + } + } + else -> resources.getString(R.string.all_submissions) } } @@ -403,4 +600,9 @@ class SubmissionListViewModel @Inject constructor( } } } + + companion object { + // Fixed seed ensures consistent ordering across app restarts for anonymous grading + private const val ANONYMOUS_SHUFFLE_SEED = 1234 + } } \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/TeacherDiscussionRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/TeacherDiscussionRouter.kt index 8d1514499d..4d9f230c36 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/TeacherDiscussionRouter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/TeacherDiscussionRouter.kt @@ -33,6 +33,17 @@ class TeacherDiscussionRouter(private val activity: FragmentActivity) : Discussi override fun routeToGroupDiscussion(group: Group, id: Long, header: DiscussionTopicHeader, isRedesign: Boolean) = Unit + override fun routeToDiscussionWebView(canvasContext: CanvasContext, discussionTopicHeaderId: Long) { + val route = DiscussionDetailsWebViewFragment.makeRoute(canvasContext, discussionTopicHeaderId) + route.apply { + removePreviousScreen = true + } + + RouteMatcher.route(activity, route) + + if (activity is FullscreenActivity) activity.finish() + } + override fun routeToNativeSpeedGrader( courseId: Long, assignmentId: Long, diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandler.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandler.kt index d95af4362d..d626ea1b82 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandler.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandler.kt @@ -17,17 +17,20 @@ package com.instructure.teacher.features.syllabus import com.instructure.canvasapi2.apis.CalendarEventAPI -import com.instructure.canvasapi2.managers.CalendarEventManager import com.instructure.canvasapi2.managers.CourseManager import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.PlannerItem import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.exhaustive import com.instructure.teacher.features.syllabus.ui.SyllabusView import com.instructure.teacher.mobius.common.ui.EffectHandler +import kotlinx.coroutines.async import kotlinx.coroutines.launch -class SyllabusEffectHandler : EffectHandler() { +class SyllabusEffectHandler( + private val repository: SyllabusRepository +) : EffectHandler() { override fun accept(effect: SyllabusEffect) { when (effect) { @@ -59,16 +62,14 @@ class SyllabusEffectHandler : EffectHandler>, eventsResult: DataResult>): DataResult.Success> { + private fun createSuccessResult( + assignmentsResult: DataResult>, + eventsResult: DataResult>, + plannerItemsResult: DataResult> + ): DataResult.Success> { val assignments = assignmentsResult.dataOrNull ?: emptyList() val events = eventsResult.dataOrNull ?: emptyList() - val combinedList = (assignments + events).sorted() + val plannerItems = plannerItemsResult.dataOrNull + ?.filter { + it.plannableType != com.instructure.canvasapi2.models.PlannableType.ASSIGNMENT && + it.plannableType != com.instructure.canvasapi2.models.PlannableType.CALENDAR_EVENT + } + ?.map { it.toScheduleItem() } ?: emptyList() + val combinedList = (assignments + events + plannerItems).sorted() return DataResult.Success(combinedList) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusPresenter.kt index 4dedba6a66..fe830a39b5 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusPresenter.kt @@ -93,6 +93,8 @@ class SyllabusPresenter : Presenter { private fun getAssignmentIcon(assignment: Assignment): Int { return when { assignment.getSubmissionTypes().contains(Assignment.SubmissionType.ONLINE_QUIZ) -> R.drawable.ic_quiz + assignment.getSubmissionTypes().contains(Assignment.SubmissionType.EXTERNAL_TOOL) && + assignment.externalToolAttributes?.url?.contains("quiz-lti") == true -> R.drawable.ic_quiz assignment.getSubmissionTypes().contains(Assignment.SubmissionType.DISCUSSION_TOPIC) -> R.drawable.ic_discussion else -> R.drawable.ic_assignment } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusRepository.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusRepository.kt new file mode 100644 index 0000000000..75033839c4 --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/SyllabusRepository.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.teacher.features.syllabus + +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.apis.PlannerAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.PlannerItem +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.depaginate +import javax.inject.Inject + +class SyllabusRepository @Inject constructor( + private val plannerApi: PlannerAPI.PlannerInterface, + private val calendarEventApi: CalendarEventAPI.CalendarEventInterface +) { + + suspend fun getPlannerItems( + startDate: String?, + endDate: String?, + contextCodes: List, + filter: String?, + forceNetwork: Boolean + ): DataResult> { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return plannerApi.getPlannerItems( + startDate, + endDate, + contextCodes, + filter, + restParams + ).depaginate { plannerApi.nextPagePlannerItems(it, restParams) } + } + + suspend fun getCalendarEvents( + allEvents: Boolean, + type: CalendarEventAPI.CalendarEventType, + startDate: String?, + endDate: String?, + contextCodes: List, + forceNetwork: Boolean + ): DataResult> { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return calendarEventApi.getCalendarEvents( + allEvents, + type.apiName, + startDate, + endDate, + contextCodes, + restParams + ).depaginate { calendarEventApi.next(it, restParams) } + } +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/ui/SyllabusFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/ui/SyllabusFragment.kt index 99157d601a..dd808b4416 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/ui/SyllabusFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/ui/SyllabusFragment.kt @@ -18,7 +18,6 @@ package com.instructure.teacher.features.syllabus.ui import android.view.LayoutInflater import android.view.ViewGroup -import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.utils.pageview.PageView import com.instructure.canvasapi2.utils.pageview.PageViewUrlParam @@ -26,19 +25,26 @@ import com.instructure.pandautils.analytics.SCREEN_VIEW_SYLLABUS import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.ParcelableArg -import com.instructure.pandautils.utils.withArgs import com.instructure.teacher.databinding.FragmentSyllabusBinding -import com.instructure.teacher.features.syllabus.* +import com.instructure.teacher.features.syllabus.SyllabusEffect +import com.instructure.teacher.features.syllabus.SyllabusEffectHandler +import com.instructure.teacher.features.syllabus.SyllabusEvent +import com.instructure.teacher.features.syllabus.SyllabusModel +import com.instructure.teacher.features.syllabus.SyllabusPresenter +import com.instructure.teacher.features.syllabus.SyllabusRepository +import com.instructure.teacher.features.syllabus.SyllabusUpdate import com.instructure.teacher.mobius.common.ui.MobiusFragment @PageView("{canvasContext}/assignments/syllabus") @ScreenView(SCREEN_VIEW_SYLLABUS) -class SyllabusFragment : MobiusFragment() { +abstract class SyllabusFragment : MobiusFragment() { @get:PageViewUrlParam("canvasContext") val canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) - override fun makeEffectHandler() = SyllabusEffectHandler() + protected abstract fun getRepository(): SyllabusRepository + + override fun makeEffectHandler() = SyllabusEffectHandler(getRepository()) override fun makeUpdate() = SyllabusUpdate() @@ -57,16 +63,4 @@ class SyllabusFragment : MobiusFragment. + * + */ + +package com.instructure.teacher.features.syllabus.ui + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.withArgs +import com.instructure.teacher.features.syllabus.SyllabusRepository +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class SyllabusRepositoryFragment : SyllabusFragment() { + + @Inject + lateinit var syllabusRepository: SyllabusRepository + + override fun getRepository() = syllabusRepository + + companion object { + fun newInstance(canvasContext: CanvasContext?) = if (isValidRoute(canvasContext)) createFragmentWithCanvasContext(canvasContext) else null + + private fun isValidRoute(canvasContext: CanvasContext?) = canvasContext is Course + + private fun createFragmentWithCanvasContext(canvasContext: CanvasContext?): SyllabusRepositoryFragment { + return SyllabusRepositoryFragment().withArgs { + putParcelable(Const.CANVAS_CONTEXT, canvasContext) + } + } + } +} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt index 775d78a9e8..6c6389925d 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt @@ -59,7 +59,7 @@ import com.instructure.teacher.databinding.FragmentCourseBrowserBinding import com.instructure.teacher.events.CourseUpdatedEvent import com.instructure.teacher.factory.CourseBrowserPresenterFactory import com.instructure.teacher.features.modules.list.ui.ModuleListFragment -import com.instructure.teacher.features.syllabus.ui.SyllabusFragment +import com.instructure.teacher.features.syllabus.ui.SyllabusRepositoryFragment import com.instructure.teacher.holders.CourseBrowserViewHolder import com.instructure.teacher.presenters.CourseBrowserPresenter import com.instructure.teacher.router.RouteMatcher @@ -270,7 +270,7 @@ class CourseBrowserFragment : BaseSyncFragment< presenter.handleStudentViewClick() } Tab.SYLLABUS_ID -> { - RouteMatcher.route(requireActivity(), Route(SyllabusFragment::class.java, presenter.canvasContext, presenter.canvasContext.makeBundle())) + RouteMatcher.route(requireActivity(), Route(SyllabusRepositoryFragment::class.java, presenter.canvasContext, presenter.canvasContext.makeBundle())) } else -> { if (tab.type == Tab.TYPE_EXTERNAL) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt index eb150acd28..2ecbcaf3df 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt @@ -345,9 +345,9 @@ class FileListFragment : BaseSyncFragment< }) } - override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { + override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { workInfoLiveData.observe(viewLifecycleOwner) { - if (it.state == WorkInfo.State.SUCCEEDED) { + if (it?.state == WorkInfo.State.SUCCEEDED) { presenter.refresh(true) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/NotATeacherFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/NotATeacherFragment.kt index cdc3c5e645..80178a5f0b 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/NotATeacherFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/NotATeacherFragment.kt @@ -19,6 +19,8 @@ package com.instructure.teacher.fragments import android.content.Intent import android.net.Uri import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.View import com.instructure.loginapi.login.tasks.LogoutTask import com.instructure.pandautils.analytics.SCREEN_VIEW_NOT_A_TEACHER @@ -53,19 +55,11 @@ class NotATeacherFragment : BaseFragment() { super.onViewCreated(view, savedInstanceState) parentLink.setOnClickListener { - startActivity(playStoreIntent(PARENT_ID)) - TeacherLogoutTask( - LogoutTask.Type.LOGOUT_NO_LOGIN_FLOW, - alarmScheduler = alarmScheduler - ).execute() + openApp(PARENT_ID) } studentLink.setOnClickListener { - startActivity(playStoreIntent(CANVAS_ID)) - TeacherLogoutTask( - LogoutTask.Type.LOGOUT_NO_LOGIN_FLOW, - alarmScheduler = alarmScheduler - ).execute() + openApp(CANVAS_ID) } login.setOnClickListener { @@ -77,4 +71,20 @@ class NotATeacherFragment : BaseFragment() { } private fun playStoreIntent(s: String): Intent = Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(MARKET_URI_PREFIX + s) } + + private fun openApp(packageName: String) { + val launchIntent = requireContext().packageManager.getLaunchIntentForPackage(packageName) + if (launchIntent != null) { + startActivity(launchIntent) + } else { + startActivity(playStoreIntent(packageName)) + } + TeacherLogoutTask( + LogoutTask.Type.LOGOUT_NO_LOGIN_FLOW, + alarmScheduler = alarmScheduler + ).execute() + + // Finish the activity so it's removed from the stack + requireActivity().finish() + } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt index d6449d2753..bc2cef8fd5 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt @@ -38,6 +38,7 @@ import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.features.file.download.FileDownloadWorker import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.fragments.BasePresenterFragment +import com.instructure.pandautils.utils.FeatureFlagProvider import com.instructure.pandautils.utils.ParcelableArg import com.instructure.pandautils.utils.PermissionUtils import com.instructure.pandautils.utils.StringArg @@ -64,12 +65,15 @@ import com.instructure.teacher.utils.setupBackButtonWithExpandCollapseAndBack import com.instructure.teacher.utils.setupMenu import com.instructure.teacher.utils.updateToolbarExpandCollapseIcon import com.instructure.teacher.viewinterface.PageDetailsView +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Job import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import javax.inject.Inject @PageView +@AndroidEntryPoint @ScreenView(SCREEN_VIEW_PAGE_DETAILS) class PageDetailsFragment : BasePresenterFragment< PageDetailsPresenter, @@ -87,6 +91,9 @@ class PageDetailsFragment : BasePresenterFragment< private var loadHtmlJob: Job? = null + @Inject + lateinit var featureFlagProvider: FeatureFlagProvider + @PageViewUrl @Suppress("unused") fun makePageViewUrl(): String { @@ -201,11 +208,11 @@ class PageDetailsFragment : BasePresenterFragment< override fun populatePageDetails(page: Page) { this.page = page - loadHtmlJob = binding.canvasWebViewWraper.webView.loadHtmlWithIframes(requireContext(), page.body, { + loadHtmlJob = binding.canvasWebViewWraper.webView.loadHtmlWithIframes(requireContext(), featureFlagProvider, page.body, { if (view != null) binding.canvasWebViewWraper.loadHtml(it, page.title, baseUrl = this.page.htmlUrl) - }) { + }, onLtiButtonPressed = { RouteMatcher.route(requireActivity(), LtiLaunchFragment.makeSessionlessLtiUrlRoute(requireActivity(), canvasContext, it)) - } + }, courseId = canvasContext.id) setupToolbar() } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizDetailsFragment.kt index f5750c18b1..3802c47204 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizDetailsFragment.kt @@ -45,6 +45,7 @@ import com.instructure.pandautils.features.speedgrader.SubmissionListFilter import com.instructure.pandautils.fragments.BasePresenterFragment import com.instructure.pandautils.utils.AssignmentGradedEvent import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.FeatureFlagProvider import com.instructure.pandautils.utils.LongArg import com.instructure.pandautils.utils.NullableParcelableArg import com.instructure.pandautils.utils.ParcelableArg @@ -80,6 +81,7 @@ import com.instructure.teacher.utils.shuffleAnswersDisplayable import com.instructure.teacher.utils.updateToolbarExpandCollapseIcon import com.instructure.teacher.view.QuizSubmissionGradedEvent import com.instructure.teacher.viewinterface.QuizDetailsView +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Job import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -88,8 +90,10 @@ import java.io.UnsupportedEncodingException import java.net.URI import java.net.URL import java.util.Date +import javax.inject.Inject @PageView +@AndroidEntryPoint @ScreenView(SCREEN_VIEW_EDIT_QUIZ_DETAILS) class QuizDetailsFragment : BasePresenterFragment< QuizDetailsPresenter, @@ -98,6 +102,9 @@ class QuizDetailsFragment : BasePresenterFragment< QuizDetailsView, Identity { + @Inject + lateinit var featureFlagProvider: FeatureFlagProvider + private var canvasContext: CanvasContext? by NullableParcelableArg(key = Const.CANVAS_CONTEXT) private var course: Course by ParcelableArg(default = Course()) @@ -133,6 +140,7 @@ class QuizDetailsFragment : BasePresenterFragment< setupToolbar() binding.swipeRefreshLayout.isRefreshing = true + binding.quizPreviewButton.root.setGone() } override fun onPresenterPrepared(presenter: QuizDetailsPresenter) = Unit @@ -425,11 +433,11 @@ class QuizDetailsFragment : BasePresenterFragment< instructionsWebViewWrapper.setBackgroundResource(android.R.color.transparent) // Load instructions - loadHtmlJob = instructionsWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), quiz.description, { + loadHtmlJob = instructionsWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), featureFlagProvider, quiz.description, { instructionsWebViewWrapper.loadHtml(it, quiz.title, baseUrl = this@QuizDetailsFragment.quiz.htmlUrl) - }) { + }, onLtiButtonPressed = { RouteMatcher.route(requireActivity(), LtiLaunchFragment.makeSessionlessLtiUrlRoute(requireActivity(), canvasContext, it)) - } + }, courseId = canvasContext?.id) } private fun setupListeners(quiz: Quiz) = with(binding) { @@ -468,6 +476,7 @@ class QuizDetailsFragment : BasePresenterFragment< RouteMatcher.route(requireActivity(), Route(QuizPreviewWebviewFragment::class.java, course, args)) } catch (e: UnsupportedEncodingException) {} } + quizPreviewButton.root.setVisible() } private fun navigateToSubmissions(course: Course, assignment: Assignment?, filter: SubmissionListFilter) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderCommentsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderCommentsFragment.kt index 22fc3b92de..563dddce6c 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderCommentsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderCommentsFragment.kt @@ -303,8 +303,9 @@ class SpeedGraderCommentsFragment : BaseListFragment) { + override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { workInfoLiveData.observe(this) { + if (it == null) return@observe presenter.onFileUploadWorkInfoChanged(it) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderTextSubmissionFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderTextSubmissionFragment.kt index b1ef9a5910..bcda8edf03 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderTextSubmissionFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderTextSubmissionFragment.kt @@ -22,10 +22,10 @@ import android.view.View import android.view.ViewGroup import android.webkit.WebChromeClient import android.webkit.WebView -import com.instructure.pandautils.base.BaseCanvasFragment import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.analytics.SCREEN_VIEW_SPEED_GRADER_TEXT_SUBMISSION import com.instructure.pandautils.analytics.ScreenView +import com.instructure.pandautils.base.BaseCanvasFragment import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.utils.StringArg import com.instructure.pandautils.utils.setGone @@ -77,10 +77,14 @@ class SpeedGraderTextSubmissionFragment : BaseCanvasFragment(), SpeedGraderWebNa } textSubmissionWebViewWrapper.webView.canvasEmbeddedWebViewCallback = object : CanvasWebView.CanvasEmbeddedWebViewCallback { - override fun launchInternalWebViewFragment(url: String) = requireActivity().startActivity(InternalWebViewActivity.createIntent(requireActivity(), url, "", true)) + override fun launchInternalWebViewFragment(url: String) = + requireActivity().startActivity(InternalWebViewActivity.createIntent(requireActivity(), url, "", true)) + override fun shouldLaunchInternalWebViewFragment(url: String): Boolean = true } + textSubmissionWebViewWrapper.webView.setInitialScale(100) + textSubmissionWebViewWrapper.loadHtml(submissionText, getString(R.string.a11y_submissionText)) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewMediaFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewMediaFragment.kt index fd8acd8c58..caa5545e69 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewMediaFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewMediaFragment.kt @@ -82,7 +82,7 @@ class ViewMediaFragment : BaseCanvasFragment(), ShareableFile { private var editableFile: EditableFile? by NullableParcelableArg() private var mediaUri: Uri? = null - private val exoAgent get() = ExoAgent.getAgentForUri(uri) + private val exoAgent get() = ExoAgent.getAgentForUri(mediaUri ?: uri) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_speed_grader_media, container, false) @@ -144,7 +144,7 @@ class ViewMediaFragment : BaseCanvasFragment(), ShareableFile { private fun fetchMediaUri() { lifecycleScope.launch { - mediaUri = RouteUtils.getRedirectUrl(uri) + mediaUri = RouteUtils.getMediaUri(uri) if (isResumed) { attachMediaPlayer() } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/DashboardPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/DashboardPresenter.kt index 537fd0ac8a..07ae69e090 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/DashboardPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/DashboardPresenter.kt @@ -18,6 +18,8 @@ package com.instructure.teacher.presenters import com.instructure.canvasapi2.managers.CourseManager import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.DashboardCard +import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.weave.apiAsync import com.instructure.pandautils.blueprint.SyncPresenter import com.instructure.pandautils.utils.ColorApiHelper @@ -54,6 +56,7 @@ class DashboardPresenter : SyncPresenter(Course::class.java .onSuccess { courses -> // Make a call to get which courses are visible on the dashboard as well as their position loadCards(forceNetwork, courses) + storeDomainOverrides(courses) } } @@ -69,6 +72,14 @@ class DashboardPresenter : SyncPresenter(Course::class.java } } + private fun storeDomainOverrides(courses: List) { + courses.forEach { course -> + course.tabs?.find { it.tabId == Tab.ASSIGNMENTS_ID }?.domain?.let { + ApiPrefs.overrideDomains[course.id] = it + } + } + } + private fun createCourseFromDashboardCard(dashboardCard: DashboardCard, courseMap: Map): Course { val course = courseMap[dashboardCard.id] return if (course != null) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/SpeedGraderPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/SpeedGraderPresenter.kt index a1020c0740..3ff43dd409 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/SpeedGraderPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/SpeedGraderPresenter.kt @@ -56,8 +56,9 @@ class SpeedGraderPresenter( private var discussion: DiscussionTopicHeader?, private val repository: AssignmentSubmissionRepository, private val filteredSubmissionIds: LongArray, - private val filter: SubmissionListFilter, - private val filterValue: Double + private val selectedFilters: Set, + private val filterValueAbove: Double?, + private val filterValueBelow: Double? ) : Presenter { private var submissions: List = emptyList() @@ -123,19 +124,57 @@ class SpeedGraderPresenter( course = data.first assignment = data.second val allSubmissions = repository.getGradeableStudentSubmissions(assignment, courseId, false) - submissions = allSubmissions.filter { - when (filter) { - SubmissionListFilter.ALL -> true - SubmissionListFilter.LATE -> it.submission?.let { assignment.getState(it, true) in listOf( - AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED_LATE, AssignmentUtils2.ASSIGNMENT_STATE_GRADED_LATE) } ?: false - SubmissionListFilter.NOT_GRADED -> it.submission?.let { assignment.getState(it, true) in listOf( - AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED, AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED_LATE) || !it.isGradeMatchesCurrentSubmission } ?: false - SubmissionListFilter.GRADED -> it.submission?.let { assignment.getState(it, true) in listOf( - AssignmentUtils2.ASSIGNMENT_STATE_GRADED, AssignmentUtils2.ASSIGNMENT_STATE_GRADED_LATE, AssignmentUtils2.ASSIGNMENT_STATE_GRADED_MISSING, AssignmentUtils2.ASSIGNMENT_STATE_EXCUSED) && it.isGradeMatchesCurrentSubmission} ?: false - SubmissionListFilter.ABOVE_VALUE -> it.submission?.let { it.isGraded && it.score >= filterValue } ?: false - SubmissionListFilter.BELOW_VALUE -> it.submission?.let { it.isGraded && it.score < filterValue } ?: false - SubmissionListFilter.MISSING -> it.submission?.workflowState == "unsubmitted" || it.submission == null + submissions = allSubmissions.filter { gradeableSubmission -> + val submission = gradeableSubmission.submission + + // Status filters (OR logic) + val statusMatch = if (selectedFilters.contains(SubmissionListFilter.ALL)) { + true + } else { + selectedFilters.any { filter -> + when (filter) { + SubmissionListFilter.LATE -> submission?.let { + assignment.getState(it, true) in listOf( + AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED_LATE, + AssignmentUtils2.ASSIGNMENT_STATE_GRADED_LATE + ) + } ?: false + SubmissionListFilter.NOT_GRADED -> submission?.let { + assignment.getState(it, true) in listOf( + AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED, + AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED_LATE + ) || !it.isGradeMatchesCurrentSubmission + } ?: false + SubmissionListFilter.GRADED -> submission?.let { + assignment.getState(it, true) in listOf( + AssignmentUtils2.ASSIGNMENT_STATE_GRADED, + AssignmentUtils2.ASSIGNMENT_STATE_GRADED_LATE, + AssignmentUtils2.ASSIGNMENT_STATE_GRADED_MISSING, + AssignmentUtils2.ASSIGNMENT_STATE_EXCUSED + ) && it.isGradeMatchesCurrentSubmission + } ?: false + SubmissionListFilter.MISSING -> submission?.workflowState == "unsubmitted" || submission == null + SubmissionListFilter.SUBMITTED -> submission?.let { + it.workflowState != "unsubmitted" && assignment.getState(it, true) in listOf( + AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED, + AssignmentUtils2.ASSIGNMENT_STATE_SUBMITTED_LATE + ) + } ?: false + else -> true + } + } } + + // Score filters (AND logic) + val aboveMatch = filterValueAbove?.let { threshold -> + submission?.let { it.isGraded && it.score >= threshold } ?: false + } ?: true + + val belowMatch = filterValueBelow?.let { threshold -> + submission?.let { it.isGraded && it.score < threshold } ?: false + } ?: true + + statusMatch && aboveMatch && belowMatch } if (submissionId > 0 && submissions.isEmpty()) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt index c680f175e9..8db4af399a 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt @@ -85,7 +85,7 @@ import com.instructure.teacher.features.modules.list.ui.ModuleListFragment import com.instructure.teacher.features.modules.progression.ModuleProgressionFragment import com.instructure.teacher.features.postpolicies.ui.PostPolicyFragment import com.instructure.teacher.features.syllabus.edit.EditSyllabusFragment -import com.instructure.teacher.features.syllabus.ui.SyllabusFragment +import com.instructure.teacher.features.syllabus.ui.SyllabusRepositoryFragment import com.instructure.teacher.fragments.AnnouncementListFragment import com.instructure.teacher.fragments.AssigneeListFragment import com.instructure.teacher.fragments.AttendanceListFragment @@ -141,7 +141,7 @@ object RouteMatcher : BaseRouteMatcher() { routes.add(Route(courseOrGroup("/"), DashboardFragment::class.java)) routes.add(Route(courseOrGroup("/:course_id"), CourseBrowserFragment::class.java)) - routes.add(Route(courseOrGroup("/:course_id/assignments/syllabus"), SyllabusFragment::class.java)) + routes.add(Route(courseOrGroup("/:course_id/assignments/syllabus"), SyllabusRepositoryFragment::class.java)) routes.add(Route(courseOrGroup("/:course_id/modules/:module_id"), ModuleListFragment::class.java)) routes.add( diff --git a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt index 5fb0784890..e338ef5c05 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt @@ -36,7 +36,7 @@ import com.instructure.teacher.features.modules.list.ui.ModuleListFragment import com.instructure.teacher.features.modules.progression.ModuleProgressionFragment import com.instructure.teacher.features.postpolicies.ui.PostPolicyFragment import com.instructure.teacher.features.syllabus.edit.EditSyllabusFragment -import com.instructure.teacher.features.syllabus.ui.SyllabusFragment +import com.instructure.teacher.features.syllabus.ui.SyllabusRepositoryFragment import com.instructure.teacher.fragments.AnnouncementListFragment import com.instructure.teacher.fragments.AssigneeListFragment import com.instructure.teacher.fragments.AttendanceListFragment @@ -218,8 +218,8 @@ object RouteResolver { fragment = EditFileFolderFragment.newInstance(route.arguments) } else if (CreateOrEditPageDetailsFragment::class.java.isAssignableFrom(cls)) { fragment = CreateOrEditPageDetailsFragment.newInstance(route.arguments) - } else if (SyllabusFragment::class.java.isAssignableFrom(cls)) { - fragment = SyllabusFragment.newInstance(canvasContext ?: route.canvasContext) + } else if (SyllabusRepositoryFragment::class.java.isAssignableFrom(cls)) { + fragment = SyllabusRepositoryFragment.newInstance(canvasContext ?: route.canvasContext) } else if (EditSyllabusFragment::class.java.isAssignableFrom(cls)) { fragment = EditSyllabusFragment.newInstance(route.arguments) } else if (EventFragment::class.java.isAssignableFrom(cls)) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt b/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt index a14479de0b..ebbdb3e8ac 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt @@ -21,7 +21,6 @@ import android.os.Build import androidx.appcompat.app.AppCompatDelegate import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.instructure.pandautils.utils.filecache.FileCache import com.instructure.canvasapi2.utils.AnalyticsEventConstants import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.Logger @@ -36,14 +35,14 @@ import com.instructure.pandautils.utils.AppType import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.filecache.FileCache import com.instructure.teacher.BuildConfig import com.instructure.teacher.R import com.instructure.teacher.activities.InitActivity import com.instructure.teacher.tasks.TeacherLogoutTask -import com.pspdfkit.PSPDFKit -import com.pspdfkit.exceptions.InvalidPSPDFKitLicenseException -import com.pspdfkit.exceptions.PSPDFKitInitializationFailedException -import com.pspdfkit.initialization.InitializationOptions +import com.pspdfkit.Nutrient +import com.pspdfkit.exceptions.InvalidNutrientLicenseException +import com.pspdfkit.exceptions.NutrientInitializationFailedException abstract class BaseAppManager : com.instructure.canvasapi2.AppManager() { @@ -76,11 +75,11 @@ abstract class BaseAppManager : com.instructure.canvasapi2.AppManager() { ColorKeeper.defaultColor = getColorCompat(R.color.textDarkest) try { - PSPDFKit.initialize(this, InitializationOptions(licenseKey = BuildConfig.PSPDFKIT_LICENSE_KEY)) - } catch (e: PSPDFKitInitializationFailedException) { - Logger.e("Current device is not compatible with PSPDFKIT!") - } catch (e: InvalidPSPDFKitLicenseException) { - Logger.e("Invalid or Trial PSPDFKIT License!") + Nutrient.initialize(this, BuildConfig.PSPDFKIT_LICENSE_KEY) + } catch (e: NutrientInitializationFailedException) { + Logger.e("Current device is not compatible with Nutrient!") + } catch (e: InvalidNutrientLicenseException) { + Logger.e("Invalid or Trial Nutrient License!") } MasqueradeHelper.masqueradeLogoutTask = Runnable { @@ -99,6 +98,9 @@ abstract class BaseAppManager : com.instructure.canvasapi2.AppManager() { } override fun performLogoutOnAuthError() { + // Don't trigger another logout if we're already in a logout flow + if (LogoutTask.isLoggingOut) return + TeacherLogoutTask( LogoutTask.Type.LOGOUT, alarmScheduler = getScheduler() diff --git a/apps/teacher/src/main/res/layout/activity_init.xml b/apps/teacher/src/main/res/layout/activity_init.xml index 699d4456ba..46c52d9b83 100644 --- a/apps/teacher/src/main/res/layout/activity_init.xml +++ b/apps/teacher/src/main/res/layout/activity_init.xml @@ -112,6 +112,7 @@ android:id="@+id/bottomBar" android:layout_width="match_parent" android:layout_height="56dp" + android:minHeight="48dp" android:layout_alignParentBottom="true" android:background="@color/backgroundLightestElevated" app:itemIconTint="@color/textDarkest" diff --git a/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml b/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml index 8b57a0da29..3c775708d8 100644 --- a/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml +++ b/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml @@ -37,7 +37,7 @@ android:layout_marginEnd="24dp" android:layout_marginStart="24dp" android:gravity="center" - android:text="@string/not_a_teacher_tap_to_visit_play_store" + android:text="@string/notATeacherDescription" android:textColor="@color/textDark" android:textSize="16sp"/> diff --git a/apps/teacher/src/main/res/values/strings.xml b/apps/teacher/src/main/res/values/strings.xml index 249906a3c9..ae9fe72034 100644 --- a/apps/teacher/src/main/res/values/strings.xml +++ b/apps/teacher/src/main/res/values/strings.xml @@ -426,8 +426,8 @@ Submitted Late Haven\'t Submitted Yet Haven\'t Been Graded - Scored Less Than… - Scored More Than… + Scored Less than + Scored More than Scored Less Than %s Scored More Than %s Add Comment diff --git a/apps/teacher/src/test/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionRepositoryTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionRepositoryTest.kt index 0745c4a5ff..5249bdb828 100644 --- a/apps/teacher/src/test/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionRepositoryTest.kt +++ b/apps/teacher/src/test/java/com/instructure/teacher/features/assignment/submission/AssignmentSubmissionRepositoryTest.kt @@ -20,6 +20,7 @@ import com.instructure.canvasapi2.apis.CourseAPI import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.apis.SectionAPI import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager +import com.instructure.canvasapi2.managers.graphql.DifferentiationTagsManager import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Enrollment import com.instructure.canvasapi2.models.GradeableStudent @@ -49,12 +50,13 @@ class AssignmentSubmissionRepositoryTest { private val assignmentApi: AssignmentAPI.AssignmentInterface = mockk(relaxed = true) private val sectionApi: SectionAPI.SectionsInterface = mockk(relaxed = true) private val customGradeStatusesManager: CustomGradeStatusesManager = mockk(relaxed = true) + private val differentiationTagsManager: DifferentiationTagsManager = mockk(relaxed = true) private lateinit var repository: AssignmentSubmissionRepository @Before fun setup() { - repository = AssignmentSubmissionRepository(assignmentApi, enrollmentApi, courseApi, sectionApi, customGradeStatusesManager) + repository = AssignmentSubmissionRepository(assignmentApi, enrollmentApi, courseApi, sectionApi, customGradeStatusesManager, differentiationTagsManager) coEvery { sectionApi.getFirstPageSectionsList(any(), any()) diff --git a/apps/teacher/src/test/java/com/instructure/teacher/features/assignment/submission/SubmissionListViewModelTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/features/assignment/submission/SubmissionListViewModelTest.kt index dfdb5c572a..b01a527f5e 100644 --- a/apps/teacher/src/test/java/com/instructure/teacher/features/assignment/submission/SubmissionListViewModelTest.kt +++ b/apps/teacher/src/test/java/com/instructure/teacher/features/assignment/submission/SubmissionListViewModelTest.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.SavedStateHandle import com.instructure.canvasapi2.CustomGradeStatusesQuery +import com.instructure.canvasapi2.DifferentiationTagsQuery import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Enrollment @@ -205,6 +206,9 @@ class SubmissionListViewModelTest { } ) + coEvery { submissionListRepository.getSections(any(), any()) } returns emptyList() + coEvery { submissionListRepository.getDifferentiationTags(any(), any()) } returns emptyList() + setupString() } @@ -252,33 +256,33 @@ class SubmissionListViewModelTest { val expected = listOf( SubmissionUiState( - 1L, - 1L, - "Late Student", + 5L, + 5L, + "Bad Graded Student", false, null, - listOf(SubmissionTag.Late, SubmissionTag.NeedsGrading), - "-", + listOf(SubmissionTag.Graded), + "0", true ), SubmissionUiState( - 2L, - 2L, - "On Time Student", + 9L, + 9L, + "Custom Status Student", false, null, - listOf(SubmissionTag.Submitted, SubmissionTag.NeedsGrading), - "-", - true + listOf(SubmissionTag.Custom("Custom Status 1", R.drawable.ic_flag, R.color.textInfo)), + "0", + false ), SubmissionUiState( - 3L, - 3L, - "Missing Student", + 6L, + 6L, + "Excused Student", false, null, - listOf(SubmissionTag.Missing), - "-", + listOf(SubmissionTag.Excused), + "", true ), SubmissionUiState( @@ -292,33 +296,23 @@ class SubmissionListViewModelTest { true ), SubmissionUiState( - 5L, - 5L, - "Bad Graded Student", - false, - null, - listOf(SubmissionTag.Graded), - "0", - true - ), - SubmissionUiState( - 6L, - 6L, - "Excused Student", + 1L, + 1L, + "Late Student", false, null, - listOf(SubmissionTag.Excused), - "", + listOf(SubmissionTag.Late, SubmissionTag.NeedsGrading), + "-", true ), SubmissionUiState( - 7L, - 7L, - "Updated Grade Student", + 3L, + 3L, + "Missing Student", false, null, - listOf(SubmissionTag.Graded), - "10", + listOf(SubmissionTag.Missing), + "-", true ), SubmissionUiState( @@ -332,14 +326,24 @@ class SubmissionListViewModelTest { false ), SubmissionUiState( - 9L, - 9L, - "Custom Status Student", + 2L, + 2L, + "On Time Student", false, null, - listOf(SubmissionTag.Custom("Custom Status 1", R.drawable.ic_flag, R.color.textInfo)), + listOf(SubmissionTag.Submitted, SubmissionTag.NeedsGrading), "-", - false + true + ), + SubmissionUiState( + 7L, + 7L, + "Updated Grade Student", + false, + null, + listOf(SubmissionTag.Graded), + "10", + true ) ) @@ -350,11 +354,16 @@ class SubmissionListViewModelTest { fun `Filter late submissions`() = runTest { val viewModel = createViewModel() - viewModel.uiState.value.actionHandler( + viewModel.uiState.value.filtersUiState.actionHandler( SubmissionListAction.SetFilters( - SubmissionListFilter.LATE, - null, - emptyList() + selectedFilters = setOf(SubmissionListFilter.LATE), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = emptySet() ) ) @@ -378,34 +387,39 @@ class SubmissionListViewModelTest { fun `Filter graded submissions`() { val viewModel = createViewModel() - viewModel.uiState.value.actionHandler( + viewModel.uiState.value.filtersUiState.actionHandler( SubmissionListAction.SetFilters( - SubmissionListFilter.GRADED, - null, - emptyList() + selectedFilters = setOf(SubmissionListFilter.GRADED), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = emptySet() ) ) val expectedData = listOf( SubmissionUiState( - 4L, - 4L, - "Good Graded Student", + 5L, + 5L, + "Bad Graded Student", false, null, listOf(SubmissionTag.Graded), - "10", + "0", true ), SubmissionUiState( - 5L, - 5L, - "Bad Graded Student", + 9L, + 9L, + "Custom Status Student", false, null, - listOf(SubmissionTag.Graded), + listOf(SubmissionTag.Custom("Custom Status 1", R.drawable.ic_flag, R.color.textInfo)), "0", - true + false ), SubmissionUiState( 6L, @@ -418,14 +432,14 @@ class SubmissionListViewModelTest { true ), SubmissionUiState( - 9L, - 9L, - "Custom Status Student", + 4L, + 4L, + "Good Graded Student", false, null, - listOf(SubmissionTag.Custom("Custom Status 1", R.drawable.ic_flag, R.color.textInfo)), - "-", - false + listOf(SubmissionTag.Graded), + "10", + true ) ) @@ -436,11 +450,16 @@ class SubmissionListViewModelTest { fun `Filter not graded submissions`() { val viewModel = createViewModel() - viewModel.uiState.value.actionHandler( + viewModel.uiState.value.filtersUiState.actionHandler( SubmissionListAction.SetFilters( - SubmissionListFilter.NOT_GRADED, - null, - emptyList() + selectedFilters = setOf(SubmissionListFilter.NOT_GRADED), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = emptySet() ) ) @@ -456,22 +475,22 @@ class SubmissionListViewModelTest { true ), SubmissionUiState( - 2L, - 2L, - "On Time Student", + 3L, + 3L, + "Missing Student", false, null, - listOf(SubmissionTag.Submitted, SubmissionTag.NeedsGrading), + listOf(SubmissionTag.Missing), "-", true ), SubmissionUiState( - 3L, - 3L, - "Missing Student", + 2L, + 2L, + "On Time Student", false, null, - listOf(SubmissionTag.Missing), + listOf(SubmissionTag.Submitted, SubmissionTag.NeedsGrading), "-", true ), @@ -494,11 +513,16 @@ class SubmissionListViewModelTest { fun `Filter not submitted`() { val viewModel = createViewModel() - viewModel.uiState.value.actionHandler( + viewModel.uiState.value.filtersUiState.actionHandler( SubmissionListAction.SetFilters( - SubmissionListFilter.MISSING, - null, - emptyList() + selectedFilters = setOf(SubmissionListFilter.MISSING), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = emptySet() ) ) @@ -522,11 +546,16 @@ class SubmissionListViewModelTest { fun `Filter scored more than`() { val viewModel = createViewModel() - viewModel.uiState.value.actionHandler( + viewModel.uiState.value.filtersUiState.actionHandler( SubmissionListAction.SetFilters( - SubmissionListFilter.ABOVE_VALUE, - 5.0, - emptyList() + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = 5.0, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = emptySet() ) ) @@ -560,11 +589,16 @@ class SubmissionListViewModelTest { fun `Filter scored less than`() { val viewModel = createViewModel() - viewModel.uiState.value.actionHandler( + viewModel.uiState.value.filtersUiState.actionHandler( SubmissionListAction.SetFilters( - SubmissionListFilter.BELOW_VALUE, - 5.0, - emptyList() + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = null, + filterValueBelow = 5.0, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = emptySet() ) ) @@ -586,7 +620,7 @@ class SubmissionListViewModelTest { false, null, listOf(SubmissionTag.Custom("Custom Status 1", R.drawable.ic_flag, R.color.textInfo)), - "-", + "0", false ) ) @@ -640,11 +674,16 @@ class SubmissionListViewModelTest { val viewModel = createViewModel() - viewModel.uiState.value.actionHandler( + viewModel.uiState.value.filtersUiState.actionHandler( SubmissionListAction.SetFilters( - SubmissionListFilter.ALL, - null, - listOf(1L) + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = listOf(1L), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = emptySet() ) ) @@ -749,7 +788,7 @@ class SubmissionListViewModelTest { val viewModel = createViewModel() - viewModel.uiState.value.actionHandler(SubmissionListAction.SubmissionClicked(1L)) + viewModel.uiState.value.filtersUiState.actionHandler(SubmissionListAction.SubmissionClicked(1L)) val events = mutableListOf() backgroundScope.launch(testDispatcher) { @@ -761,8 +800,9 @@ class SubmissionListViewModelTest { selectedIdx = 0, anonymousGrading = false, filteredSubmissionIds = longArrayOf(1L), - SubmissionListFilter.ALL, - 0.0 + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = null, + filterValueBelow = null ), events.last() ) } @@ -787,7 +827,7 @@ class SubmissionListViewModelTest { any() ) } returns submissions - viewModel.uiState.value.actionHandler(SubmissionListAction.Refresh) + viewModel.uiState.value.filtersUiState.actionHandler(SubmissionListAction.Refresh) coVerify { submissionListRepository.getGradeableStudentSubmissions(any(), any(), true) @@ -799,7 +839,7 @@ class SubmissionListViewModelTest { @Test fun `Route to user profile`() = runTest { val viewModel = createViewModel() - viewModel.uiState.value.actionHandler(SubmissionListAction.AvatarClicked(1L)) + viewModel.uiState.value.filtersUiState.actionHandler(SubmissionListAction.AvatarClicked(1L)) val events = mutableListOf() backgroundScope.launch(testDispatcher) { @@ -811,7 +851,7 @@ class SubmissionListViewModelTest { @Test fun `Search action`() = runTest { val viewModel = createViewModel() - viewModel.uiState.value.actionHandler(SubmissionListAction.Search("On Time Student")) + viewModel.uiState.value.filtersUiState.actionHandler(SubmissionListAction.Search("On Time Student")) val expected = listOf( SubmissionUiState( @@ -833,23 +873,28 @@ class SubmissionListViewModelTest { @Test fun `Set filters update uiState`() { val viewModel = createViewModel() - viewModel.uiState.value.actionHandler( + viewModel.uiState.value.filtersUiState.actionHandler( SubmissionListAction.SetFilters( - SubmissionListFilter.ABOVE_VALUE, - 5.0, - listOf(1L) + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = 5.0, + filterValueBelow = null, + selectedSections = listOf(1L), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = emptySet() ) ) - assertEquals(SubmissionListFilter.ABOVE_VALUE, viewModel.uiState.value.filter) - assertEquals(5.0, viewModel.uiState.value.filterValue) - assertEquals(listOf(1L), viewModel.uiState.value.selectedSections) + assertEquals(setOf(SubmissionListFilter.ALL), viewModel.uiState.value.filtersUiState.selectedFilters) + assertEquals(5.0, viewModel.uiState.value.filtersUiState.filterValueAbove) + assertEquals(listOf(1L), viewModel.uiState.value.filtersUiState.initialSelectedSections) } @Test fun `Open post policy`() = runTest { val viewModel = createViewModel() - viewModel.uiState.value.actionHandler(SubmissionListAction.ShowPostPolicy) + viewModel.uiState.value.filtersUiState.actionHandler(SubmissionListAction.ShowPostPolicy) val events = mutableListOf() backgroundScope.launch(testDispatcher) { @@ -872,15 +917,20 @@ class SubmissionListViewModelTest { val viewModel = createViewModel() - viewModel.uiState.value.actionHandler( + viewModel.uiState.value.filtersUiState.actionHandler( SubmissionListAction.SetFilters( - SubmissionListFilter.NOT_GRADED, - null, - emptyList() + selectedFilters = setOf(SubmissionListFilter.NOT_GRADED), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = emptySet() ) ) - viewModel.uiState.value.actionHandler(SubmissionListAction.SendMessage) + viewModel.uiState.value.filtersUiState.actionHandler(SubmissionListAction.SendMessage) val expectedRecipients = listOf(submissions[0], submissions[1], submissions[2], submissions[6]).map { @@ -1044,7 +1094,7 @@ class SubmissionListViewModelTest { fun `Set filter to 'All' if nothing is set`() { every { savedStateHandle.get(SubmissionListFragment.FILTER_TYPE) } returns null val viewModel = createViewModel() - assertEquals(SubmissionListFilter.ALL, viewModel.uiState.value.filter) + assertEquals(setOf(SubmissionListFilter.ALL), viewModel.uiState.value.filtersUiState.selectedFilters) } @Test @@ -1094,7 +1144,7 @@ class SubmissionListViewModelTest { val viewModel = createViewModel() - val excepted = listOf( + val expected = listOf( SubmissionUiState( 1L, 1L, @@ -1127,10 +1177,525 @@ class SubmissionListViewModelTest { ) ) - excepted.forEach { + expected.forEach { assert(viewModel.uiState.value.submissions.contains(it)) } - assertEquals(true, viewModel.uiState.value.anonymousGrading) + assertEquals(true, viewModel.uiState.value.filtersUiState.anonymousGrading) + } + + @Test + fun `Filter by custom grade status`() = runTest { + val viewModel = createViewModel() + + viewModel.uiState.value.filtersUiState.actionHandler( + SubmissionListAction.SetFilters( + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = setOf("1") + ) + ) + + val expected = listOf( + SubmissionUiState( + 9L, + 9L, + "Custom Status Student", + false, + null, + listOf(SubmissionTag.Custom("Custom Status 1", R.drawable.ic_flag, R.color.textInfo)), + "0", + false + ) + ) + + assertEquals(expected, viewModel.uiState.value.submissions) + } + + @Test + fun `Custom status filter uses OR logic with standard filters`() = runTest { + val viewModel = createViewModel() + + viewModel.uiState.value.filtersUiState.actionHandler( + SubmissionListAction.SetFilters( + selectedFilters = setOf(SubmissionListFilter.LATE), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = setOf("1") + ) + ) + + // Should show both LATE submissions AND custom status submissions + val expected = listOf( + SubmissionUiState( + 9L, + 9L, + "Custom Status Student", + false, + null, + listOf(SubmissionTag.Custom("Custom Status 1", R.drawable.ic_flag, R.color.textInfo)), + "0", + false + ), + SubmissionUiState( + 1L, + 1L, + "Late Student", + false, + null, + listOf(SubmissionTag.Late, SubmissionTag.NeedsGrading), + "-", + true + ) + ) + + assertEquals(expected, viewModel.uiState.value.submissions) + } + + @Test + fun `Custom status excludes ALL filter when selected`() = runTest { + val viewModel = createViewModel() + + viewModel.uiState.value.filtersUiState.actionHandler( + SubmissionListAction.SetFilters( + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = setOf("1") + ) + ) + + // When custom status is selected, ALL filter should be ignored + val expected = listOf( + SubmissionUiState( + 9L, + 9L, + "Custom Status Student", + false, + null, + listOf(SubmissionTag.Custom("Custom Status 1", R.drawable.ic_flag, R.color.textInfo)), + "0", + false + ) + ) + + assertEquals(expected, viewModel.uiState.value.submissions) + } + + @Test + fun `Filter by differentiation tags`() = runTest { + val diffTag1 = mockk(relaxed = true) { + every { _id } returns "tag1" + every { name } returns "Group 1" + every { membersConnection?.nodes } returns listOf( + mockk { + every { user?._id } returns "1" + } + ) + } + + coEvery { + submissionListRepository.getDifferentiationTags(any(), any()) + } returns listOf(Pair(diffTag1, "Group Set 1")) + + coEvery { + submissionListRepository.getGradeableStudentSubmissions( + any(), + any(), + any() + ) + } returns listOf( + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(1L, name = "Student 1"), + ), + submission = Submission(id = 1L, late = false, attempt = 1L), + ), + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(2L, name = "Student 2"), + ), + submission = Submission(id = 2L, late = false, attempt = 1L), + ), + ) + + val viewModel = createViewModel() + + viewModel.uiState.value.filtersUiState.actionHandler( + SubmissionListAction.SetFilters( + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = setOf("tag1"), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = emptySet() + ) + ) + + // Should only show Student 1 who belongs to tag1 + assertEquals(1, viewModel.uiState.value.submissions.size) + assertEquals("Student 1", viewModel.uiState.value.submissions[0].userName) + } + + @Test + fun `Filter students without differentiation tags`() = runTest { + val diffTag1 = mockk(relaxed = true) { + every { _id } returns "tag1" + every { name } returns "Group 1" + every { membersConnection?.nodes } returns listOf( + mockk { + every { user?._id } returns "1" + } + ) + } + + coEvery { + submissionListRepository.getDifferentiationTags(any(), any()) + } returns listOf(Pair(diffTag1, "Group Set 1")) + + coEvery { + submissionListRepository.getGradeableStudentSubmissions( + any(), + any(), + any() + ) + } returns listOf( + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(1L, name = "Student 1"), + ), + submission = Submission(id = 1L, late = false, attempt = 1L), + ), + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(2L, name = "Student 2"), + ), + submission = Submission(id = 2L, late = false, attempt = 1L), + ), + ) + + val viewModel = createViewModel() + + viewModel.uiState.value.filtersUiState.actionHandler( + SubmissionListAction.SetFilters( + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = true, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = emptySet() + ) + ) + + // Should only show Student 2 who doesn't belong to any tag + assertEquals(1, viewModel.uiState.value.submissions.size) + assertEquals("Student 2", viewModel.uiState.value.submissions[0].userName) + } + + @Test + fun `Filter differentiation tags with OR logic`() = runTest { + val diffTag1 = mockk(relaxed = true) { + every { _id } returns "tag1" + every { name } returns "Group 1" + every { membersConnection?.nodes } returns listOf( + mockk { + every { user?._id } returns "1" + } + ) + } + + coEvery { + submissionListRepository.getDifferentiationTags(any(), any()) + } returns listOf(Pair(diffTag1, "Group Set 1")) + + coEvery { + submissionListRepository.getGradeableStudentSubmissions( + any(), + any(), + any() + ) + } returns listOf( + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(1L, name = "Student 1"), + ), + submission = Submission(id = 1L, late = false, attempt = 1L), + ), + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(2L, name = "Student 2"), + ), + submission = Submission(id = 2L, late = false, attempt = 1L), + ), + ) + + val viewModel = createViewModel() + + viewModel.uiState.value.filtersUiState.actionHandler( + SubmissionListAction.SetFilters( + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = setOf("tag1"), + includeStudentsWithoutTags = true, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = emptySet() + ) + ) + + // Should show both Student 1 (in tag) AND Student 2 (without tag) + assertEquals(2, viewModel.uiState.value.submissions.size) + } + + @Test + fun `Sort by student name`() = runTest { + coEvery { + submissionListRepository.getGradeableStudentSubmissions( + any(), + any(), + any() + ) + } returns listOf( + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(1L, name = "Charlie Student"), + ), + submission = Submission(id = 1L, late = false, attempt = 1L), + ), + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(2L, name = "Alice Student"), + ), + submission = Submission(id = 2L, late = false, attempt = 1L), + ), + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(3L, name = "Bob Student"), + ), + submission = Submission(id = 3L, late = false, attempt = 1L), + ), + ) + + val viewModel = createViewModel() + + viewModel.uiState.value.filtersUiState.actionHandler( + SubmissionListAction.SetFilters( + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_NAME, + selectedCustomStatusIds = emptySet() + ) + ) + + val submissions = viewModel.uiState.value.submissions + assertEquals("Alice Student", submissions[0].userName) + assertEquals("Bob Student", submissions[1].userName) + assertEquals("Charlie Student", submissions[2].userName) + } + + @Test + fun `Sort by submission date`() = runTest { + coEvery { + submissionListRepository.getGradeableStudentSubmissions( + any(), + any(), + any() + ) + } returns listOf( + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(1L, name = "Student 1"), + ), + submission = Submission(id = 1L, late = false, attempt = 1L, submittedAt = Date(100)), + ), + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(2L, name = "Student 2"), + ), + submission = Submission(id = 2L, late = false, attempt = 1L, submittedAt = Date(300)), + ), + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(3L, name = "Student 3"), + ), + submission = Submission(id = 3L, late = false, attempt = 1L, submittedAt = Date(200)), + ), + ) + + val viewModel = createViewModel() + + viewModel.uiState.value.filtersUiState.actionHandler( + SubmissionListAction.SetFilters( + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.SUBMISSION_DATE, + selectedCustomStatusIds = emptySet() + ) + ) + + val submissions = viewModel.uiState.value.submissions + // Should be sorted by submission date descending (most recent first) + assertEquals("Student 2", submissions[0].userName) // Date(300) + assertEquals("Student 3", submissions[1].userName) // Date(200) + assertEquals("Student 1", submissions[2].userName) // Date(100) + } + + @Test + fun `Sort by submission status`() = runTest { + coEvery { + submissionListRepository.getGradeableStudentSubmissions( + any(), + any(), + any() + ) + } returns listOf( + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(1L, name = "Submitted Student"), + ), + submission = Submission(id = 1L, late = false, attempt = 1L), + ), + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(2L, name = "Missing Student"), + ), + submission = Submission(id = 2L, missing = true), + ), + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(3L, name = "Late Student"), + ), + submission = Submission(id = 3L, late = true, attempt = 1L), + ), + GradeableStudentSubmission( + assignee = StudentAssignee( + student = User(4L, name = "Not Submitted Student"), + ), + submission = null, + ), + ) + + val viewModel = createViewModel() + + viewModel.uiState.value.filtersUiState.actionHandler( + SubmissionListAction.SetFilters( + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.SUBMISSION_STATUS, + selectedCustomStatusIds = emptySet() + ) + ) + + val submissions = viewModel.uiState.value.submissions + // Order: 0=submitted, 1=late, 2=missing, 3=not submitted + assertEquals("Submitted Student", submissions[0].userName) + assertEquals("Late Student", submissions[1].userName) + assertEquals("Missing Student", submissions[2].userName) + assertEquals("Not Submitted Student", submissions[3].userName) + } + + @Test + fun `Multiple status filters use OR logic`() = runTest { + val viewModel = createViewModel() + + viewModel.uiState.value.filtersUiState.actionHandler( + SubmissionListAction.SetFilters( + selectedFilters = setOf(SubmissionListFilter.LATE, SubmissionListFilter.MISSING), + filterValueAbove = null, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = emptySet() + ) + ) + + // Should show LATE submissions OR MISSING submissions + val submissions = viewModel.uiState.value.submissions + val userNames = submissions.map { it.userName } + + assert(userNames.contains("Late Student")) + assert(userNames.contains("Not Submitted Student")) + assertEquals(2, submissions.size) + } + + @Test + fun `Filter by scored above and status filters combined`() = runTest { + val viewModel = createViewModel() + + viewModel.uiState.value.filtersUiState.actionHandler( + SubmissionListAction.SetFilters( + selectedFilters = setOf(SubmissionListFilter.GRADED), + filterValueAbove = 5.0, + filterValueBelow = null, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = emptySet() + ) + ) + + // Should show graded submissions that scored more than 5 + val submissions = viewModel.uiState.value.submissions + assertEquals(1, submissions.size) + assertEquals("Good Graded Student", submissions[0].userName) + assertEquals("10", submissions[0].grade) + } + + @Test + fun `Filter by scored below and custom status combined`() = runTest { + val viewModel = createViewModel() + + viewModel.uiState.value.filtersUiState.actionHandler( + SubmissionListAction.SetFilters( + selectedFilters = setOf(SubmissionListFilter.ALL), + filterValueAbove = null, + filterValueBelow = 5.0, + selectedSections = emptyList(), + selectedDifferentiationTagIds = emptySet(), + includeStudentsWithoutTags = false, + sortOrder = SubmissionSortOrder.STUDENT_SORTABLE_NAME, + selectedCustomStatusIds = setOf("1") + ) + ) + + // Should show submissions with custom status AND score below 5 + // Custom Status Student has no grade (score = null), Bad Graded Student has score = 0 + val submissions = viewModel.uiState.value.submissions + // Only Custom Status Student should appear (Bad Graded Student is filtered out by custom status filter) + assertEquals(1, submissions.size) + assertEquals("Custom Status Student", submissions[0].userName) } @Test @@ -1157,7 +1722,7 @@ class SubmissionListViewModelTest { val viewModel = createViewModel() - val excepted = SubmissionUiState( + val expected = SubmissionUiState( 1L, 1L, "Student 1", @@ -1168,7 +1733,7 @@ class SubmissionListViewModelTest { true ) - assertEquals(excepted, viewModel.uiState.value.submissions.first()) + assertEquals(expected, viewModel.uiState.value.submissions.first()) } private fun createViewModel(): SubmissionListViewModel { diff --git a/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandlerTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandlerTest.kt index 526f9c469c..c438c258b0 100644 --- a/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandlerTest.kt +++ b/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusEffectHandlerTest.kt @@ -16,22 +16,36 @@ */ package com.instructure.teacher.features.syllabus -import com.instructure.canvasapi2.apis.CalendarEventAPI -import com.instructure.canvasapi2.managers.CalendarEventManager import com.instructure.canvasapi2.managers.CourseManager -import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.Plannable +import com.instructure.canvasapi2.models.PlannableType +import com.instructure.canvasapi2.models.PlannerItem +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.toApiString import com.instructure.teacher.features.syllabus.ui.SyllabusView import com.spotify.mobius.functions.Consumer -import io.mockk.* +import io.mockk.coEvery +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.slot +import io.mockk.unmockkAll +import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.test.setMain +import org.junit.After import org.junit.Before import org.junit.Test -import java.util.* +import java.util.Date import java.util.concurrent.Executors private const val COURSE_ID: Long = 1L @@ -39,8 +53,9 @@ private const val COURSE_ID: Long = 1L class SyllabusEffectHandlerTest { private val view: SyllabusView = mockk(relaxed = true) private val eventConsumer: Consumer = mockk(relaxed = true) + private val repository: SyllabusRepository = mockk(relaxed = true) - private val effectHandler = SyllabusEffectHandler().apply { + private val effectHandler = SyllabusEffectHandler(repository).apply { view = this@SyllabusEffectHandlerTest.view connect(eventConsumer) } @@ -54,6 +69,13 @@ class SyllabusEffectHandlerTest { fun setup() { Dispatchers.setMain(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) course = Course(id = COURSE_ID) + mockkObject(ApiPrefs) + every { ApiPrefs.fullDomain } returns "https://test.instructure.com" + } + + @After + fun tearDown() { + unmockkAll() } @Test @@ -93,10 +115,8 @@ class SyllabusEffectHandlerTest { coEvery { await() } returns DataResult.Success(permissions) } - mockkObject(CalendarEventManager) - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), any(), any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail() - } + coEvery { repository.getCalendarEvents(any(), any(), any(), any(), any(), any()) } returns DataResult.Fail() + coEvery { repository.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Fail() // When effectHandler.accept(SyllabusEffect.LoadData(COURSE_ID, false)) @@ -139,13 +159,12 @@ class SyllabusEffectHandlerTest { coEvery { await() } returns DataResult.Success(permissions) } - mockkObject(CalendarEventManager) - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.ASSIGNMENT, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(assignments) - } - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.CALENDAR, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(calendarEvents) - } + coEvery { repository.getCalendarEvents(true, any(), any(), any(), any(), false) } returnsMany listOf( + DataResult.Success(assignments), + DataResult.Success(calendarEvents) + ) + + coEvery { repository.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Success(emptyList()) // When effectHandler.accept(SyllabusEffect.LoadData(COURSE_ID, false)) @@ -181,13 +200,12 @@ class SyllabusEffectHandlerTest { coEvery { await() } returns DataResult.Success(permissions) } - mockkObject(CalendarEventManager) - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.ASSIGNMENT, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(assignments) - } - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.CALENDAR, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail() - } + coEvery { repository.getCalendarEvents(true, any(), any(), any(), any(), false) } returnsMany listOf( + DataResult.Success(assignments), + DataResult.Fail() + ) + + coEvery { repository.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Success(emptyList()) // When effectHandler.accept(SyllabusEffect.LoadData(COURSE_ID, false)) @@ -217,13 +235,12 @@ class SyllabusEffectHandlerTest { coEvery { await() } returns DataResult.Success(permissions) } - mockkObject(CalendarEventManager) - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.ASSIGNMENT, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail() - } - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.CALENDAR, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(calendarEvents) - } + coEvery { repository.getCalendarEvents(true, any(), any(), any(), any(), false) } returnsMany listOf( + DataResult.Fail(), + DataResult.Success(calendarEvents) + ) + + coEvery { repository.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Success(emptyList()) // When effectHandler.accept(SyllabusEffect.LoadData(COURSE_ID, false)) @@ -254,14 +271,6 @@ class SyllabusEffectHandlerTest { coEvery { await() } returns DataResult.Success(permissions) } - mockkObject(CalendarEventManager) - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.ASSIGNMENT, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(assignments) - } - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.CALENDAR, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(calendarEvents) - } - // When effectHandler.accept(SyllabusEffect.LoadData(COURSE_ID, false)) @@ -274,6 +283,147 @@ class SyllabusEffectHandlerTest { confirmVerified(eventConsumer) } + @Test + fun `LoadData should filter out assignment and calendar event type planner items to avoid duplicates`() { + // Given + val now = Date().time + val assignment = ScheduleItem( + itemId = "123", + itemType = ScheduleItem.Type.TYPE_ASSIGNMENT, + startAt = Date(now + 1000).toApiString() + ) + val calendarEvent = ScheduleItem( + itemId = "789", + itemType = ScheduleItem.Type.TYPE_CALENDAR, + startAt = Date(now + 2000).toApiString() + ) + val plannerAssignment = PlannerItem( + courseId = COURSE_ID, + groupId = null, + userId = null, + contextType = "Course", + contextName = "Test Course", + plannableType = PlannableType.ASSIGNMENT, + plannable = Plannable( + id = 123, + title = "Assignment", + courseId = COURSE_ID, + groupId = null, + userId = null, + pointsPossible = null, + dueAt = null, + assignmentId = null, + todoDate = null, + startAt = null, + endAt = null, + details = null, + allDay = null + ), + plannableDate = Date(), + htmlUrl = null, + submissionState = null, + newActivity = false + ) + val plannerCalendarEvent = PlannerItem( + courseId = COURSE_ID, + groupId = null, + userId = null, + contextType = "Course", + contextName = "Test Course", + plannableType = PlannableType.CALENDAR_EVENT, + plannable = Plannable( + id = 789, + title = "Calendar Event", + courseId = COURSE_ID, + groupId = null, + userId = null, + pointsPossible = null, + dueAt = null, + assignmentId = null, + todoDate = null, + startAt = null, + endAt = null, + details = null, + allDay = null + ), + plannableDate = Date(), + htmlUrl = null, + submissionState = null, + newActivity = false + ) + val plannerQuiz = PlannerItem( + courseId = COURSE_ID, + groupId = null, + userId = null, + contextType = "Course", + contextName = "Test Course", + plannableType = PlannableType.QUIZ, + plannable = Plannable( + id = 456, + title = "Quiz", + courseId = COURSE_ID, + groupId = null, + userId = null, + pointsPossible = null, + dueAt = null, + assignmentId = null, + todoDate = Date(now + 3000).toApiString(), + startAt = null, + endAt = null, + details = null, + allDay = null + ), + plannableDate = Date(now + 3000), + htmlUrl = null, + submissionState = null, + newActivity = false + ) + + mockkObject(CourseManager) + every { CourseManager.getCourseWithSyllabusAsync(COURSE_ID, false) } returns mockk { + coEvery { await() } returns DataResult.Success(course) + } + every { CourseManager.getCourseSettingsAsync(COURSE_ID, false) } returns mockk { + coEvery { await() } returns DataResult.Success(CourseSettings(courseSummary = true)) + } + every { CourseManager.getPermissionsAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(permissions) + } + + coEvery { repository.getCalendarEvents(true, any(), any(), any(), any(), false) } returnsMany listOf( + DataResult.Success(listOf(assignment)), + DataResult.Success(listOf(calendarEvent)) + ) + + coEvery { repository.getPlannerItems(any(), any(), any(), any(), any()) } returns DataResult.Success(listOf(plannerAssignment, plannerCalendarEvent, plannerQuiz)) + + // When + effectHandler.accept(SyllabusEffect.LoadData(COURSE_ID, false)) + + // Then + // Capture what was actually called + val slot = slot() + verify(timeout = 100) { + eventConsumer.accept(capture(slot)) + } + + // Verify the captured event + val event = slot.captured + assert(event.course is DataResult.Success) + assert(event.events is DataResult.Success) + assert(event.permissionsResult is DataResult.Success) + assert(event.summaryAllowed == true) + + // Verify we have exactly 3 items: assignment, calendar event, and quiz (no planner duplicates) + val items = event.events.dataOrNull!! + assert(items.size == 3) { "Expected 3 items, got ${items.size}" } + assert(items.count { it.itemType == ScheduleItem.Type.TYPE_ASSIGNMENT } == 1) { "Expected 1 assignment" } + assert(items.count { it.itemType == ScheduleItem.Type.TYPE_CALENDAR } == 1) { "Expected 1 calendar event" } + assert(items.count { it.itemType == ScheduleItem.Type.TYPE_SYLLABUS } == 1) { "Expected 1 quiz/syllabus item" } + + confirmVerified(eventConsumer) + } + @Test fun `ShowAssignmentView results in view calling showAssignmentView`() { // Given diff --git a/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusRepositoryTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusRepositoryTest.kt new file mode 100644 index 0000000000..3fd20f4752 --- /dev/null +++ b/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusRepositoryTest.kt @@ -0,0 +1,409 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.teacher.features.syllabus + +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.apis.PlannerAPI +import com.instructure.canvasapi2.models.Plannable +import com.instructure.canvasapi2.models.PlannableType +import com.instructure.canvasapi2.models.PlannerItem +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.toApiString +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.util.Date + +class SyllabusRepositoryTest { + + private val plannerApi: PlannerAPI.PlannerInterface = mockk(relaxed = true) + private val calendarEventApi: CalendarEventAPI.CalendarEventInterface = mockk(relaxed = true) + + private lateinit var repository: SyllabusRepository + + @Before + fun setup() { + repository = SyllabusRepository(plannerApi, calendarEventApi) + } + + @Test + fun `getPlannerItems returns success when API call succeeds`() = runTest { + val contextCodes = listOf("course_1") + val expectedItems = listOf( + PlannerItem( + courseId = 1L, + groupId = null, + userId = null, + contextType = "Course", + contextName = "Test Course", + plannableType = PlannableType.QUIZ, + plannable = Plannable( + id = 123, + title = "Test Quiz", + courseId = 1L, + groupId = null, + userId = null, + pointsPossible = 10.0, + dueAt = null, + assignmentId = null, + todoDate = Date().toApiString(), + startAt = null, + endAt = null, + details = null, + allDay = null + ), + plannableDate = Date(), + htmlUrl = null, + submissionState = null, + newActivity = false + ) + ) + + coEvery { + plannerApi.getPlannerItems(any(), any(), any(), any(), any()) + } returns DataResult.Success(expectedItems) + + val result = repository.getPlannerItems( + startDate = null, + endDate = null, + contextCodes = contextCodes, + filter = "all_ungraded_todo_items", + forceNetwork = false + ) + + assertTrue(result is DataResult.Success) + assertEquals(expectedItems, result.dataOrNull) + coVerify(exactly = 1) { + plannerApi.getPlannerItems( + null, + null, + contextCodes, + "all_ungraded_todo_items", + any() + ) + } + } + + @Test + fun `getPlannerItems returns failure when API call fails`() = runTest { + val contextCodes = listOf("course_1") + + coEvery { + plannerApi.getPlannerItems(any(), any(), any(), any(), any()) + } returns DataResult.Fail() + + val result = repository.getPlannerItems( + startDate = null, + endDate = null, + contextCodes = contextCodes, + filter = "all_ungraded_todo_items", + forceNetwork = true + ) + + assertTrue(result is DataResult.Fail) + coVerify(exactly = 1) { + plannerApi.getPlannerItems( + null, + null, + contextCodes, + "all_ungraded_todo_items", + any() + ) + } + } + + @Test + fun `getPlannerItems passes forceNetwork parameter correctly`() = runTest { + val contextCodes = listOf("course_1") + + coEvery { + plannerApi.getPlannerItems(any(), any(), any(), any(), any()) + } returns DataResult.Success(emptyList()) + + repository.getPlannerItems( + startDate = "2025-01-01", + endDate = "2025-01-31", + contextCodes = contextCodes, + filter = null, + forceNetwork = true + ) + + coVerify(exactly = 1) { + plannerApi.getPlannerItems( + "2025-01-01", + "2025-01-31", + contextCodes, + null, + match { it.isForceReadFromNetwork } + ) + } + } + + @Test + fun `getCalendarEvents returns success for ASSIGNMENT type when API call succeeds`() = runTest { + val contextCodes = listOf("course_1") + val expectedItems = listOf( + ScheduleItem( + itemId = "123", + title = "Test Assignment", + itemType = ScheduleItem.Type.TYPE_ASSIGNMENT, + contextCode = "course_1" + ) + ) + + coEvery { + calendarEventApi.getCalendarEvents(any(), any(), any(), any(), any(), any()) + } returns DataResult.Success(expectedItems) + + val result = repository.getCalendarEvents( + allEvents = true, + type = CalendarEventAPI.CalendarEventType.ASSIGNMENT, + startDate = null, + endDate = null, + contextCodes = contextCodes, + forceNetwork = false + ) + + assertTrue(result is DataResult.Success) + assertEquals(expectedItems, result.dataOrNull) + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.ASSIGNMENT.apiName, + null, + null, + contextCodes, + any() + ) + } + } + + @Test + fun `getCalendarEvents returns success for CALENDAR type when API call succeeds`() = runTest { + val contextCodes = listOf("course_1") + val expectedItems = listOf( + ScheduleItem( + itemId = "456", + title = "Test Calendar Event", + itemType = ScheduleItem.Type.TYPE_CALENDAR, + contextCode = "course_1" + ) + ) + + coEvery { + calendarEventApi.getCalendarEvents(any(), any(), any(), any(), any(), any()) + } returns DataResult.Success(expectedItems) + + val result = repository.getCalendarEvents( + allEvents = true, + type = CalendarEventAPI.CalendarEventType.CALENDAR, + startDate = null, + endDate = null, + contextCodes = contextCodes, + forceNetwork = false + ) + + assertTrue(result is DataResult.Success) + assertEquals(expectedItems, result.dataOrNull) + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.CALENDAR.apiName, + null, + null, + contextCodes, + any() + ) + } + } + + @Test + fun `getCalendarEvents returns failure when API call fails`() = runTest { + val contextCodes = listOf("course_1") + + coEvery { + calendarEventApi.getCalendarEvents(any(), any(), any(), any(), any(), any()) + } returns DataResult.Fail() + + val result = repository.getCalendarEvents( + allEvents = true, + type = CalendarEventAPI.CalendarEventType.ASSIGNMENT, + startDate = null, + endDate = null, + contextCodes = contextCodes, + forceNetwork = true + ) + + assertTrue(result is DataResult.Fail) + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.ASSIGNMENT.apiName, + null, + null, + contextCodes, + any() + ) + } + } + + @Test + fun `getCalendarEvents passes date range parameters correctly`() = runTest { + val contextCodes = listOf("course_1", "course_2") + val startDate = "2025-01-01" + val endDate = "2025-01-31" + + coEvery { + calendarEventApi.getCalendarEvents(any(), any(), any(), any(), any(), any()) + } returns DataResult.Success(emptyList()) + + repository.getCalendarEvents( + allEvents = false, + type = CalendarEventAPI.CalendarEventType.CALENDAR, + startDate = startDate, + endDate = endDate, + contextCodes = contextCodes, + forceNetwork = false + ) + + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + false, + CalendarEventAPI.CalendarEventType.CALENDAR.apiName, + startDate, + endDate, + contextCodes, + match { !it.isForceReadFromNetwork } + ) + } + } + + @Test + fun `getCalendarEvents passes forceNetwork parameter correctly`() = runTest { + val contextCodes = listOf("course_1") + + coEvery { + calendarEventApi.getCalendarEvents(any(), any(), any(), any(), any(), any()) + } returns DataResult.Success(emptyList()) + + repository.getCalendarEvents( + allEvents = true, + type = CalendarEventAPI.CalendarEventType.ASSIGNMENT, + startDate = null, + endDate = null, + contextCodes = contextCodes, + forceNetwork = true + ) + + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.ASSIGNMENT.apiName, + null, + null, + contextCodes, + match { it.isForceReadFromNetwork } + ) + } + } + + @Test + fun `getCalendarEvents converts enum to apiName correctly`() = runTest { + val contextCodes = listOf("course_1") + + coEvery { + calendarEventApi.getCalendarEvents(any(), any(), any(), any(), any(), any()) + } returns DataResult.Success(emptyList()) + + // Test ASSIGNMENT type + repository.getCalendarEvents( + allEvents = true, + type = CalendarEventAPI.CalendarEventType.ASSIGNMENT, + startDate = null, + endDate = null, + contextCodes = contextCodes, + forceNetwork = false + ) + + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + any(), + "assignment", + any(), + any(), + any(), + any() + ) + } + + // Test CALENDAR type + repository.getCalendarEvents( + allEvents = true, + type = CalendarEventAPI.CalendarEventType.CALENDAR, + startDate = null, + endDate = null, + contextCodes = contextCodes, + forceNetwork = false + ) + + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + any(), + "event", + any(), + any(), + any(), + any() + ) + } + } + + @Test + fun `getCalendarEvents handles multiple context codes correctly`() = runTest { + val contextCodes = listOf("course_1", "course_2", "course_3") + + coEvery { + calendarEventApi.getCalendarEvents(any(), any(), any(), any(), any(), any()) + } returns DataResult.Success(emptyList()) + + repository.getCalendarEvents( + allEvents = true, + type = CalendarEventAPI.CalendarEventType.ASSIGNMENT, + startDate = null, + endDate = null, + contextCodes = contextCodes, + forceNetwork = false + ) + + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + any(), + any(), + any(), + any(), + contextCodes, + any() + ) + } + } +} diff --git a/automation/dataseedingapi/src/main/graphql/com/instructure/dataseeding/CreateDiscussionTopicMinimal.graphql b/automation/dataseedingapi/src/main/graphql/com/instructure/dataseeding/CreateDiscussionTopicMinimal.graphql new file mode 100644 index 0000000000..2bbb4ee622 --- /dev/null +++ b/automation/dataseedingapi/src/main/graphql/com/instructure/dataseeding/CreateDiscussionTopicMinimal.graphql @@ -0,0 +1,43 @@ +# +# Copyright (C) 2025 - present Instructure, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +mutation CreateDiscussionTopicMinimal( + $contextId: ID!, + $contextType: DiscussionTopicContextType!, + $title: String!, + $assignment: AssignmentCreate!, + $checkpoints: [DiscussionCheckpoints!]! +) { + createDiscussionTopic( + input: { + contextId: $contextId, + contextType: $contextType, + title: $title, + published: true, + assignment: $assignment, + checkpoints: $checkpoints + } + ) { + discussionTopic { + _id + title + } + errors { + attribute + message + } + } +} diff --git a/automation/dataseedingapi/src/main/graphql/com/instructure/dataseeding/CreateDiscussionTopicWithCheckpoints.graphql b/automation/dataseedingapi/src/main/graphql/com/instructure/dataseeding/CreateDiscussionTopicWithCheckpoints.graphql new file mode 100644 index 0000000000..1c146b0632 --- /dev/null +++ b/automation/dataseedingapi/src/main/graphql/com/instructure/dataseeding/CreateDiscussionTopicWithCheckpoints.graphql @@ -0,0 +1,63 @@ +# +# Copyright (C) 2025 - present Instructure, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +mutation CreateDiscussionTopicWithCheckpoints( + $contextId: ID!, + $contextType: DiscussionTopicContextType!, + $title: String!, + $assignment: AssignmentCreate!, + $checkpoints: [DiscussionCheckpoints!]! +) { + createDiscussionTopic( + input: { + contextId: $contextId, + contextType: $contextType, + title: $title, + published: true, + discussionType: threaded, + podcastEnabled: false, + podcastHasStudentPosts: false, + isAnnouncement: false, + locked: false, + requireInitialPost: false, + allowRating: false, + onlyGradersCanRate: false, + expanded: true, + expandedLocked: false, + sortOrder: asc, + sortOrderLocked: false, + isAnonymousAuthor: false, + anonymousState: off, + specificSections: "all", + assignment: $assignment, + checkpoints: $checkpoints + } + ) { + discussionTopic { + _id + contextType + title + assignment { + _id + name + } + } + errors { + attribute + message + } + } +} \ No newline at end of file diff --git a/automation/dataseedingapi/src/main/graphql/com/instructure/dataseeding/schema.json b/automation/dataseedingapi/src/main/graphql/com/instructure/dataseeding/schema.json index 10c65ed4b5..9f01f89725 100644 --- a/automation/dataseedingapi/src/main/graphql/com/instructure/dataseeding/schema.json +++ b/automation/dataseedingapi/src/main/graphql/com/instructure/dataseeding/schema.json @@ -1,3286 +1,1564 @@ { - "__schema": { - "queryType": { - "name": "Query" - }, - "mutationType": { - "name": "Mutation" - }, - "subscriptionType": null, - "types": [ - { - "kind": "OBJECT", - "name": "Account", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "accountDomainLookups", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + "data": { + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": { + "name": "Mutation" + }, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "Account", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "AccountDomainLookup", + "kind": "SCALAR", + "name": "ID", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "accountDomains", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "accountDomainLookups", + "description": null, + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { - "kind": "OBJECT", - "name": "AccountDomain", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "AccountDomainLookup", + "ofType": null + } } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "coursesConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CourseConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcomeCalculationMethod", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "OutcomeCalculationMethod", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcomeProficiency", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "OutcomeProficiency", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "parentAccountsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "accountDomains", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "AccountDomain", + "ofType": null + } + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "coursesConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "OBJECT", - "name": "AccountConnection", + "name": "CourseConnection", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "proficiencyRatingsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "ProficiencyRatingConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rootOutcomeGroup", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customGradeStatusesConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "OBJECT", - "name": "LearningOutcomeGroup", + "name": "CustomGradeStatusConnection", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sisId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "subAccountsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "AccountConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AccountConnection", - "description": "The connection type for Account.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "AccountEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Account", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AccountDomain", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "host", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AccountDomainLookup", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "accountDomain", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "AccountDomain", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "authenticationProvider", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AccountEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "Account", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "AddConversationMessageInput", - "description": "Autogenerated input type of AddConversationMessage", - "fields": null, - "inputFields": [ - { - "name": "conversationId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "instructureIdentityOrganizationId", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "body", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "name", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "recipients", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeCalculationMethod", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "OutcomeCalculationMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeProficiency", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "OutcomeProficiency", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "parentAccountsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { "kind": "SCALAR", "name": "String", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } - } - }, - "defaultValue": null - }, - { - "name": "includedMessages", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + ], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "ID", + "kind": "OBJECT", + "name": "AccountConnection", "ofType": null } - } - }, - "defaultValue": null - }, - { - "name": "attachmentIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "proficiencyRatingsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } - } - }, - "defaultValue": null - }, - { - "name": "mediaCommentId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "mediaCommentType", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "userNote", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AddConversationMessagePayload", - "description": "Autogenerated return type of AddConversationMessage", - "fields": [ - { - "name": "conversationMessage", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "ConversationMessage", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + ], + "type": { + "kind": "OBJECT", + "name": "ProficiencyRatingConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rootOutcomeGroup", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", - "name": "ValidationError", + "name": "LearningOutcomeGroup", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AdhocStudents", - "description": "A list of students that an `AssignmentOverride` applies to", - "fields": [ - { - "name": "students", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "User", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubricsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } - } + ], + "type": { + "kind": "OBJECT", + "name": "RubricConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AnonymousUser", - "description": null, - "fields": [ - { - "name": "avatarUrl", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "shortName", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "sisId", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AssessmentRequest", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "user", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "standardGradeStatusesConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "OBJECT", - "name": "User", + "name": "StandardGradeStatusConnection", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "workflowState", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "AssessmentType", - "description": "The type of assessment", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "grading", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "peer_review", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "provisional_grade", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "INTERFACE", - "name": "AssetString", - "description": null, - "fields": [ - { - "name": "assetString", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": [ - { - "kind": "OBJECT", - "name": "Course", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Enrollment", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Group", - "ofType": null - } - ] - }, - { - "kind": "OBJECT", - "name": "Assignment", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subAccountsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "AccountConnection", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "allowGoogleDocsSubmission", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "allowedAttempts", - "description": "The number of submission attempts a student can make for this assignment. null implies unlimited.", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", + { + "kind": "INTERFACE", + "name": "Node", "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "allowedExtensions", - "description": "permitted uploaded file extensions (e.g. ['doc', 'xls', 'txt'])", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AccountConnection", + "description": "The connection type for Account.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "AccountEdge", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "anonymizeStudents", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "anonymousGrading", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "anonymousInstructorAnnotations", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assessmentRequestsForCurrentUser", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { "kind": "OBJECT", - "name": "AssessmentRequest", + "name": "Account", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assignmentGroup", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "AssignmentGroup", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assignmentOverrides", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AccountDomain", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "AssignmentOverrideConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "canDuplicate", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "canUnpublish", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "course", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Course", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "discussion", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Discussion", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "dueAt", - "description": "when this assignment is due", - "args": [ - { - "name": "applyOverrides", - "description": "When true, return the overridden dates.\n\nNot all roles have permission to view un-overridden dates (in which\ncase the overridden dates will be returned)\n", - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": "true" - } - ], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "dueDateRequired", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "expectsExternalSubmission", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "expectsSubmission", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradeGroupStudentsIndividually", - "description": "If this is a group assignment, boolean flag indicating whether or not students will be graded individually.", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradingType", - "description": null, - "args": [], - "type": { - "kind": "ENUM", - "name": "GradingType", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "groupSet", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "GroupSet", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "groupSubmissionsConnection", - "description": "returns submissions grouped to one submission object per group", - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "host", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null - }, - "defaultValue": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "SubmissionSearchFilterInput", - "ofType": null - }, - "defaultValue": null + } }, - { - "name": "orderBy", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "SubmissionSearchOrder", - "ofType": null - } - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "SubmissionConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "hasSubmittedSubmissions", - "description": "If true, the assignment has been submitted to by at least one student", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "htmlUrl", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inClosedGradingPeriod", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lockAt", - "description": "the lock date (assignment is locked after this date)", - "args": [ - { - "name": "applyOverrides", - "description": "When true, return the overridden dates.\n\nNot all roles have permission to view un-overridden dates (in which\ncase the overridden dates will be returned)\n", - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": "true" - } - ], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lockInfo", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "LockInfo", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "moderatedGrading", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "ModeratedGrading", + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "modules", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AccountDomainLookup", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "Module", + "kind": "SCALAR", + "name": "ID", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "needsGradingCount", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nonDigitalSubmission", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "omitFromFinalGrade", - "description": "If true, the assignment will be omitted from the student's final grade", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "onlyVisibleToOverrides", - "description": "specifies that this assignment is only assigned to students for whom an\n `AssignmentOverride` applies.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "peerReviews", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "PeerReviews", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pointsPossible", - "description": "the assignment is out of this many points", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "position", - "description": "determines the order this assignment is displayed in in its assignment group", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "postPolicy", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "PostPolicy", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "accountDomain", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "AccountDomain", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "postToSis", - "description": "present if Sync Grades to SIS feature is enabled", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "quiz", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Quiz", - "ofType": null + { + "name": "authenticationProvider", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rubric", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Rubric", - "ofType": null + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rubricAssociation", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "RubricAssociation", - "ofType": null + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sisId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "state", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "AssignmentState", + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionTypes", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AccountEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "ENUM", - "name": "SubmissionType", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionsConnection", - "description": "submissions for this assignment", - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { "kind": "SCALAR", "name": "String", "ofType": null - }, - "defaultValue": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Account", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ActivityStream", + "description": "An activity stream", + "fields": [ + { + "name": "summary", + "description": "Returns a summary of the activity stream items for the current context", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "StreamSummaryItem", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "AddConversationMessageInput", + "description": "Autogenerated input type of AddConversationMessage", + "fields": null, + "inputFields": [ + { + "name": "attachmentIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "body", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextCode", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "conversationId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "SubmissionSearchFilterInput", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "orderBy", - "description": null, - "type": { - "kind": "LIST", + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "includedMessages", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", "name": null, "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "SubmissionSearchOrder", - "ofType": null - } + "kind": "SCALAR", + "name": "ID", + "ofType": null } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "SubmissionConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionsDownloads", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "timeZoneEdited", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unlockAt", - "description": "the unlock date (assignment is unlocked after this date)", - "args": [ - { - "name": "applyOverrides", - "description": "When true, return the overridden dates.\n\nNot all roles have permission to view un-overridden dates (in which\ncase the overridden dates will be returned)\n", - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": "true" - } - ], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "ModuleItemInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AssignmentConnection", - "description": "The connection type for Assignment.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "AssignmentEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AssignmentEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "AssignmentFilter", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "gradingPeriodId", - "description": "only return assignments for the given grading period. Defaults to\nthe current grading period. Pass `null` to return all assignments\n(irrespective of the assignment's grading period)\n", - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AssignmentGroup", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "mediaCommentId", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assignmentsConnection", - "description": "returns a list of assignments.\n\n**NOTE**: for courses with grading periods, this will only return grading\nperiods in the current course; see `AssignmentFilter` for more info.\nIn courses with grading periods that don't have students, it is necessary\nto *not* filter by grading period to list assignments.\n", - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { + { + "name": "mediaCommentType", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "recipients", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AddConversationMessagePayload", + "description": "Autogenerated return type of AddConversationMessage.", + "fields": [ + { + "name": "conversationMessage", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "ConversationMessage", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AdhocStudents", + "description": "A list of students that an `AssignmentOverride` applies to", + "fields": [ + { + "name": "students", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AnonymousStudentIdentity", + "description": "An anonymous student identity", + "fields": [ + { + "name": "anonymousId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "position", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Int", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AnonymousUser", + "description": null, + "fields": [ + { + "name": "avatarUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "AssignmentFilter", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "AssignmentConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradesConnection", - "description": "grades for this assignment group", - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "shortName", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null - }, - "defaultValue": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AssessmentRequest", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "GradesEnrollmentFilter", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "GradesConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "groupWeight", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "position", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rules", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "AssignmentGroupRules", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sisId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "state", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "AssignmentGroupState", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "AssignmentsConnectionInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AssignmentGroupConnection", - "description": "The connection type for AssignmentGroup.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "AssignmentGroupEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anonymizedUser", + "description": null, + "args": [], + "type": { "kind": "OBJECT", - "name": "AssignmentGroup", + "name": "User", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AssignmentGroupEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "AssignmentGroup", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AssignmentGroupRules", - "description": null, - "fields": [ - { - "name": "dropHighest", - "description": "The highest N assignments are not included in grade calculations", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "dropLowest", - "description": "The lowest N assignments are not included in grade calculations", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "neverDrop", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + { + "name": "anonymousId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assetId", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "Assignment", + "kind": "SCALAR", + "name": "String", "ofType": null } - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "AssignmentGroupState", - "description": "States that Assignment Group can be in", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "available", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleted", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "AssignmentModeratedGradingUpdate", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "enabled", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "graderCount", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "graderCommentsVisibleToGraders", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "graderNamesVisibleToFinalGrader", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "gradersAnonymousToGraders", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "finalGraderId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AssignmentOverride", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "assetSubmissionType", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "allDay", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assignment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "dueAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": "ID of the object.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "available", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lockAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "set", - "description": "This object specifies what students this override applies to", - "args": [], - "type": { - "kind": "UNION", - "name": "AssignmentOverrideSet", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "title", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unlockAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AssignmentOverrideConnection", - "description": "The connection type for AssignmentOverride.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "AssignmentOverrideEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "AssignmentOverride", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "AssignmentOverrideCreateOrUpdate", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "dueAt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "lockAt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "unlockAt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "sectionId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "groupId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "studentIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "ID", + "kind": "OBJECT", + "name": "User", "ofType": null } - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AssignmentOverrideEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "AssignmentOverride", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "UNION", - "name": "AssignmentOverrideSet", - "description": "Objects that can be assigned overridden dates", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": [ - { - "kind": "OBJECT", - "name": "AdhocStudents", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Group", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Noop", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Section", - "ofType": null - } - ] - }, - { - "kind": "INPUT_OBJECT", - "name": "AssignmentPeerReviewsUpdate", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "enabled", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "count", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "dueAt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "intraReviews", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "anonymousReviews", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "automaticReviews", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "AssignmentState", - "description": "States that an Assignment can be in", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "unpublished", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "published", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleted", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "INTERFACE", - "name": "AssignmentsConnectionInterface", - "description": null, - "fields": [ - { - "name": "assignmentsConnection", - "description": "returns a list of assignments.\n\n**NOTE**: for courses with grading periods, this will only return grading\nperiods in the current course; see `AssignmentFilter` for more info.\nIn courses with grading periods that don't have students, it is necessary\nto *not* filter by grading period to list assignments.\n", - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "workflowState", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null - }, - "defaultValue": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "AssessmentType", + "description": "The type of assessment", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "grading", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "peer_review", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "provisional_grade", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "self_assessment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INTERFACE", + "name": "AssetString", + "description": null, + "fields": [ + { + "name": "assetString", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "AssignmentFilter", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { "kind": "OBJECT", - "name": "AssignmentConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": [ - { - "kind": "OBJECT", - "name": "AssignmentGroup", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Course", - "ofType": null - } - ] - }, - { - "kind": "OBJECT", - "name": "AuditLogs", - "description": null, - "fields": [ - { - "name": "mutationLogs", - "description": "A list of all recent graphql mutations run on the specified object", - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "name": "Course", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Enrollment", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Group", + "ofType": null + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Assignment", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allowGoogleDocsSubmission", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allowedAttempts", + "description": "The number of submission attempts a student can make for this assignment. null implies unlimited.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null }, - { - "name": "assetString", - "description": null, - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allowedExtensions", + "description": "permitted uploaded file extensions (e.g. ['doc', 'xls', 'txt'])", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "NON_NULL", "name": null, "ofType": { @@ -3288,1122 +1566,286 @@ "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "startTime", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null + } }, - { - "name": "endTime", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "MutationLogConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "AutoLeaderPolicy", - "description": "Determines if/how a leader is chosen for each group", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "random", - "description": "a leader is chosen at random", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "first", - "description": "the first student assigned to the group is the leader", - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "Boolean", - "description": "Represents `true` or `false` values.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CommentBankItem", - "description": "Comment bank items", - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "comment", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "anonymizeStudents", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "courseId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "userId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "anonymousGrading", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CommentBankItemConnection", - "description": "The connection type for CommentBankItem.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "CommentBankItemEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "CommentBankItem", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CommentBankItemEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "CommentBankItem", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CommunicationChannel", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "anonymousInstructorAnnotations", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "notificationPolicies", - "description": null, - "args": [ - { - "name": "contextType", - "description": null, - "type": { - "kind": "ENUM", - "name": "NotificationPreferencesContextType", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anonymousStudentIdentities", + "description": null, + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { - "kind": "OBJECT", - "name": "NotificationPolicy", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "notificationPolicyOverrides", - "description": null, - "args": [ - { - "name": "accountId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "courseId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "contextType", - "description": null, - "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "ENUM", - "name": "NotificationPreferencesContextType", + "kind": "OBJECT", + "name": "AnonymousStudentIdentity", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assessmentRequestsForCurrentUser", + "description": null, + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { - "kind": "OBJECT", - "name": "NotificationPolicy", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "path", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pathType", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ContentTag", - "description": "An edge in a connection.", - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "canUnlink", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "group", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "LearningOutcomeGroup", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "UNION", - "name": "ContentTagContent", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ContentTagConnection", - "description": "The connection type for ContentTagContent.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ContentTag", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "UNION", - "name": "ContentTagContent", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "UNION", - "name": "ContentTagContent", - "description": "Content of a Content Tag", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": [ - { - "kind": "OBJECT", - "name": "LearningOutcome", - "ofType": null - } - ] - }, - { - "kind": "OBJECT", - "name": "Conversation", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextName", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextType", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "conversationMessagesConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "participants", - "description": null, - "type": { - "kind": "LIST", + "kind": "NON_NULL", "name": null, "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } + "kind": "OBJECT", + "name": "AssessmentRequest", + "ofType": null } - }, - "defaultValue": null - }, - { - "name": "createdBefore", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "ConversationMessageConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "conversationParticipantsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentGroup", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "AssignmentGroup", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "ConversationParticipantConnection", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "subject", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ConversationMessage", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "assignmentGroupId", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attachmentsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentOverrides", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "AssignmentOverrideConnection", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "FileConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "author", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentTargetConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "orderBy", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignmentTargetSortOrder", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "OBJECT", - "name": "User", + "name": "AssignmentOverrideConnection", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "body", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "canDuplicate", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "conversationId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "canUnpublish", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canUpdateRubricSelfAssessment", + "description": "specifies that the current user can update the rubric self-assessment.", + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mediaComment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "MediaObject", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "recipients", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "checkpoints", + "description": null, + "args": [], + "type": { "kind": "LIST", "name": null, "ofType": { @@ -4411,2253 +1853,1297 @@ "name": null, "ofType": { "kind": "OBJECT", - "name": "User", + "name": "Checkpoint", "ofType": null } } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ConversationMessageConnection", - "description": "The connection type for ConversationMessage.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ConversationMessageEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "course", + "description": null, + "args": [], + "type": { "kind": "OBJECT", - "name": "ConversationMessage", + "name": "Course", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ConversationMessageEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "ConversationMessage", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ConversationParticipant", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "courseId", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "conversation", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Conversation", + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "description", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "label", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "messages", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "ConversationMessageConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "subscribed", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "user", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussion", + "description": null, + "args": [], + "type": { "kind": "OBJECT", - "name": "User", + "name": "Discussion", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "userId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dueAt", + "description": "when this assignment is due", + "args": [ + { + "name": "applyOverrides", + "description": "When true, return the overridden dates.\n\nNot all roles have permission to view un-overridden dates (in which\ncase the overridden dates will be returned)\n", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "true", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "workflowState", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "dueDateRequired", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ConversationParticipantConnection", - "description": "The connection type for ConversationParticipant.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ConversationParticipantEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ConversationParticipant", + { + "name": "expectsExternalSubmission", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ConversationParticipantEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "ConversationParticipant", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Course", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "expectsSubmission", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "account", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Account", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assetString", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assignmentGroupsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradeAsGroup", + "description": "specifies that students are being graded as a group (as opposed to being graded individually).", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradeByQuestionEnabled", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "Boolean", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "AssignmentGroupConnection", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assignmentPostPolicies", - "description": "PostPolicies for assignments within a course\n", - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + { + "name": "gradeGroupStudentsIndividually", + "description": "If this is a group assignment, boolean flag indicating whether or not students will be graded individually.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradedSubmissionsExist", + "description": "If true, the assignment has at least one graded submission", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "PostPolicyConnection", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assignmentsConnection", - "description": "returns a list of assignments.\n\n**NOTE**: for courses with grading periods, this will only return grading\nperiods in the current course; see `AssignmentFilter` for more info.\nIn courses with grading periods that don't have students, it is necessary\nto *not* filter by grading period to list assignments.\n", - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + { + "name": "gradesPublished", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingPeriodId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingStandard", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "GradingStandard", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingType", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "GradingType", + "ofType": null }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "AssignmentFilter", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "AssignmentConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "courseCode", - "description": "course short name", - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "enrollmentsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupCategoryId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupSet", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "GroupSet", + "ofType": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupSubmissionsConnection", + "description": "returns submissions grouped to one submission object per group", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "SubmissionSearchFilterInput", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "orderBy", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SubmissionSearchOrder", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SubmissionConnection", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasGroupCategory", + "description": "specifies that this assignment is a group assignment", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "Boolean", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "EnrollmentFilterInput", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "EnrollmentConnection", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "externalToolsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + { + "name": "hasMultipleDueDates", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasSubAssignments", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "Boolean", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "ExternalToolFilterInput", - "ofType": null - }, - "defaultValue": "{}" - } - ], - "type": { - "kind": "OBJECT", - "name": "ExternalToolConnection", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradingPeriodsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + { + "name": "hasSubmittedSubmissions", + "description": "If true, the assignment has been submitted to by at least one student", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "htmlUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "GradingPeriodConnection", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "groupSetsConnection", - "description": "Project group sets for this course.\n", - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + { + "name": "importantDates", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inClosedGradingPeriod", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isLockedByMasterCourse", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "Boolean", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "GroupSetConnection", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "groupsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + { + "name": "isNewQuiz", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lockAt", + "description": "the lock date (assignment is locked after this date)", + "args": [ + { + "name": "applyOverrides", + "description": "When true, return the overridden dates.\n\nNot all roles have permission to view un-overridden dates (in which\ncase the overridden dates will be returned)\n", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "true", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lockInfo", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "LockInfo", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "GroupConnection", - "ofType": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "moderatedGrading", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "ModeratedGrading", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "moderatedGradingEnabled", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "imageUrl", - "description": "Returns a URL for the course image (this is the image used on dashboard\ncourse cards)\n", - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "modulesConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "modules", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + } + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mySubAssignmentSubmissionsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SubmissionConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "needsGradingCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nonDigitalSubmission", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "omitFromFinalGrade", + "description": "If true, the assignment will be omitted from the student's final grade", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onlyVisibleToOverrides", + "description": "specifies that this assignment is only assigned to students for whom an\n `AssignmentOverride` applies.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "Boolean", "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "ModuleConnection", - "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "originalityReportVisibility", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "peerReviews", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "PeerReviews", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcomeCalculationMethod", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "OutcomeCalculationMethod", - "ofType": null + { + "name": "pointsPossible", + "description": "the assignment is out of this many points", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcomeProficiency", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "OutcomeProficiency", - "ofType": null + { + "name": "position", + "description": "determines the order this assignment is displayed in in its assignment group", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "permissions", - "description": "returns permission information for the current user in this course", - "args": [], - "type": { - "kind": "OBJECT", - "name": "CoursePermissions", - "ofType": null + { + "name": "postManually", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postPolicy", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "PostPolicy", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "postPolicy", - "description": "A course-specific post policy", - "args": [], - "type": { - "kind": "OBJECT", - "name": "PostPolicy", - "ofType": null + { + "name": "postToSis", + "description": "present if Sync Grades to SIS feature is enabled", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rootOutcomeGroup", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "published", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "quiz", + "description": null, + "args": [], + "type": { "kind": "OBJECT", - "name": "LearningOutcomeGroup", + "name": "Quiz", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sectionsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "restrictQuantitativeData", + "description": "Is the current user restricted from viewing quantitative data", + "args": [ + { + "name": "checkExtraPermissions", + "description": "Check extra permissions in RQD method", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubric", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Rubric", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "SectionConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sisId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "state", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "CourseWorkflowState", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionsConnection", - "description": "all the submissions for assignments in this course", - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubricAssessment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "AssignmentRubricAssessment", + "ofType": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubricAssociation", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "RubricAssociation", + "ofType": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubricSelfAssessmentEnabled", + "description": "specifies that students can self-assess using the assignment rubric.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubricUpdateUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "scoreStatistic", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "AssignmentScoreStatistic", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sisId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "AssignmentState", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "studentIds", - "description": "Only return submissions for the given students.", - "type": { - "kind": "LIST", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", "name": null, "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } + "kind": "ENUM", + "name": "SubmissionType", + "ofType": null } - }, - "defaultValue": null + } }, - { - "name": "orderBy", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionsConnection", + "description": "submissions for this assignment", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "SubmissionSearchFilterInput", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "orderBy", + "description": null, + "type": { + "kind": "LIST", "name": null, "ofType": { - "kind": "INPUT_OBJECT", - "name": "SubmissionOrderCriteria", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SubmissionSearchOrder", + "ofType": null + } } - } - }, - "defaultValue": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SubmissionConnection", + "ofType": null }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "SubmissionFilterInput", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "SubmissionConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "term", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Term", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "usersConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "userIds", - "description": "Only include users with the given ids.\n\n**This field is deprecated, use `filter: {userIds}` instead.**\n", - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - } - }, - "defaultValue": null - }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "CourseUsersFilter", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "UserConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "AssetString", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "AssignmentsConnectionInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CourseConnection", - "description": "The connection type for Course.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "CourseEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Course", + { + "name": "submissionsDownloads", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CourseEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "Course", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "CourseFilterableEnrollmentState", - "description": "Users in a course can be returned based on these enrollment states", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "invited", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "creation_pending", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "active", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rejected", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "completed", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inactive", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CoursePermissions", - "description": null, - "fields": [ - { - "name": "becomeUser", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "manageGrades", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sendMessages", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "viewAllGrades", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "viewAnalytics", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CourseUsersFilter", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "userIds", - "description": "only include users with the given ids", - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "supportsGradeByQuestion", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null } - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "enrollmentStates", - "description": "only return users with the given enrollment state. defaults\nto `invited`, `creation_pending`, `active`\n", - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "CourseFilterableEnrollmentState", - "ofType": null - } - } + { + "name": "timeZoneEdited", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "CourseWorkflowState", - "description": "States that Courses can be in", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "created", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "claimed", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "available", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "completed", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleted", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CreateAccountDomainLookupInput", - "description": "Autogenerated input type of CreateAccountDomainLookup", - "fields": null, - "inputFields": [ - { - "name": "accountDomainId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "title", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "authenticationProvider", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + { + "name": "totalGradedSubmissions", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "name", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "totalSubmissions", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "String", + "name": "Int", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CreateAccountDomainLookupPayload", - "description": "Autogenerated return type of CreateAccountDomainLookup", - "fields": [ - { - "name": "accountDomainLookup", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "AccountDomainLookup", - "ofType": null + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unlockAt", + "description": "the unlock date (assignment is unlocked after this date)", + "args": [ + { + "name": "applyOverrides", + "description": "When true, return the overridden dates.\n\nNot all roles have permission to view un-overridden dates (in which\ncase the overridden dates will be returned)\n", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "true", + "isDeprecated": false, + "deprecationReason": null } - } + ], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CreateAssignmentInput", - "description": "Autogenerated input type of CreateAssignment", - "fields": null, - "inputFields": [ - { - "name": "state", - "description": null, - "type": { - "kind": "ENUM", - "name": "AssignmentState", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "dueAt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "lockAt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "unlockAt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "description", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "assignmentOverrides", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "AssignmentOverrideCreateOrUpdate", - "ofType": null - } - } - }, - "defaultValue": null - }, - { - "name": "position", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "pointsPossible", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "gradingType", - "description": null, - "type": { - "kind": "ENUM", - "name": "GradingType", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "allowedExtensions", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "visibleToEveryone", + "description": "specifies all other variables that can determine visiblity.", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null } - } - }, - "defaultValue": null - }, - { - "name": "assignmentGroupId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "groupSetId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", "ofType": null }, - "defaultValue": null - }, - { - "name": "allowedAttempts", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", + { + "kind": "INTERFACE", + "name": "ModuleItemInterface", "ofType": null }, - "defaultValue": null - }, - { - "name": "onlyVisibleToOverrides", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", + { + "kind": "INTERFACE", + "name": "Node", "ofType": null }, - "defaultValue": null - }, - { - "name": "submissionTypes", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "SubmissionType", - "ofType": null - } - } - }, - "defaultValue": null - }, - { - "name": "peerReviews", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "AssignmentPeerReviewsUpdate", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "moderatedGrading", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "AssignmentModeratedGradingUpdate", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "gradeGroupStudentsIndividually", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "omitFromFinalGrade", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "anonymousInstructorAnnotations", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "postToSis", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "anonymousGrading", - "description": "requires anonymous_marking course feature to be set to true", - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "moduleIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AssignmentConnection", + "description": "The connection type for Assignment.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { - "kind": "SCALAR", - "name": "ID", + "kind": "OBJECT", + "name": "AssignmentEdge", "ofType": null } - } - }, - "defaultValue": null - }, - { - "name": "courseId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "name", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CreateAssignmentPayload", - "description": "Autogenerated return type of CreateAssignment", - "fields": [ - { - "name": "assignment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { "kind": "OBJECT", - "name": "ValidationError", + "name": "Assignment", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CreateCommentBankItemInput", - "description": "Autogenerated input type of CreateCommentBankItem", - "fields": null, - "inputFields": [ - { - "name": "courseId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "comment", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CreateCommentBankItemPayload", - "description": "Autogenerated return type of CreateCommentBankItem", - "fields": [ - { - "name": "commentBankItem", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "CommentBankItem", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", - "name": "ValidationError", + "name": "PageInfo", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CreateConversationInput", - "description": "Autogenerated input type of CreateConversation", - "fields": null, - "inputFields": [ - { - "name": "recipients", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "AssignmentCreate", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "abGuid", + "description": null, + "type": { "kind": "LIST", "name": null, "ofType": { @@ -6669,2088 +3155,1275 @@ "ofType": null } } - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "subject", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + { + "name": "assignmentGroupId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentOverrides", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AssignmentOverrideCreateOrUpdate", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "body", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "dueAt", + "description": null, + "type": { "kind": "SCALAR", - "name": "String", + "name": "DateTime", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "bulkMessage", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + { + "name": "forCheckpoints", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "forceNew", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + { + "name": "suppressAssignment", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "groupConversation", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + { + "name": "gradingStandardId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingType", + "description": null, + "type": { + "kind": "ENUM", + "name": "GradingType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "attachmentIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - } + { + "name": "groupCategoryId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "mediaCommentId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null + { + "name": "importantDates", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "mediaCommentType", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + { + "name": "intraReviews", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "contextCode", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + { + "name": "lockAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "conversationId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null + { + "name": "onlyVisibleToOverrides", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "userNote", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + { + "name": "peerReviews", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignmentPeerReviewsUpdate", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postToSis", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "tags", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + { + "name": "unlockAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseId", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null } - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CreateConversationPayload", - "description": "Autogenerated return type of CreateConversation", - "fields": [ - { - "name": "conversations", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ConversationParticipant", + "kind": "SCALAR", + "name": "String", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AssignmentEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", + "kind": "SCALAR", + "name": "String", "ofType": null } - } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "AssignmentFilter", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "userId", + "description": "only return assignments for the given user. Defaults to\nthe current user.\n", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CreateDiscussionEntryDraftInput", - "description": "Autogenerated input type of CreateDiscussionEntryDraft", - "fields": null, - "inputFields": [ - { - "name": "discussionTopicId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "gradingPeriodId", + "description": "only return assignments for the given grading period. Defaults to\nthe current grading period. Pass `null` to return all assignments\n(irrespective of the assignment's grading period)\n", + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "discussionEntryId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "parentId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "fileId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "message", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "includeReplyPreview", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CreateDiscussionEntryDraftPayload", - "description": "Autogenerated return type of CreateDiscussionEntryDraft", - "fields": [ - { - "name": "discussionEntryDraft", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "DiscussionEntryDraft", - "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + { + "name": "searchTerm", + "description": "only return assignments whose title matches this search term\n", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AssignmentGroup", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", + "kind": "SCALAR", + "name": "ID", "ofType": null } - } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentsConnection", + "description": "returns a list of assignments.\n\n**NOTE**: for courses with grading periods, this will only return grading\nperiods in the current course; see `AssignmentFilter` for more info.\nIn courses with grading periods that don't have students, it is necessary\nto *not* filter by grading period to list assignments.\n", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignmentFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "AssignmentConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CreateDiscussionEntryInput", - "description": "Autogenerated input type of CreateDiscussionEntry", - "fields": null, - "inputFields": [ - { - "name": "discussionTopicId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "createdAt", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "message", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "parentEntryId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "fileId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "includeReplyPreview", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CreateDiscussionEntryPayload", - "description": "Autogenerated return type of CreateDiscussionEntry", - "fields": [ - { - "name": "discussionEntry", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "DiscussionEntry", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradesConnection", + "description": "grades for this assignment group", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "GradesEnrollmentFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "GradesConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + { + "name": "groupWeight", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", + "kind": "SCALAR", + "name": "ID", "ofType": null } - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CreateGroupInSetInput", - "description": "Autogenerated input type of CreateGroupInSet", - "fields": null, - "inputFields": [ - { - "name": "name", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "groupSetId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "name", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CreateGroupInSetPayload", - "description": "Autogenerated return type of CreateGroupInSet", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + { + "name": "position", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rules", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "AssignmentGroupRules", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sisId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", + "kind": "ENUM", + "name": "AssignmentGroupState", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "group", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Group", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CreateLearningOutcomeGroupInput", - "description": "Autogenerated input type of CreateLearningOutcomeGroup", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "title", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "String", + "name": "DateTime", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "AssignmentsConnectionInterface", + "ofType": null }, - "defaultValue": null - }, - { - "name": "description", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", "ofType": null }, - "defaultValue": null - }, - { - "name": "vendorGuid", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", + { + "kind": "INTERFACE", + "name": "Node", "ofType": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CreateLearningOutcomeGroupPayload", - "description": "Autogenerated return type of CreateLearningOutcomeGroup", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AssignmentGroupConnection", + "description": "The connection type for AssignmentGroup.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { "kind": "OBJECT", - "name": "ValidationError", + "name": "AssignmentGroupEdge", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "learningOutcomeGroup", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "LearningOutcomeGroup", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CreateLearningOutcomeInput", - "description": "Autogenerated input type of CreateLearningOutcome", - "fields": null, - "inputFields": [ - { - "name": "groupId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "title", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "description", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "displayName", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "vendorGuid", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "calculationMethod", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "calculationInt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "rubricCriterion", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "RubricCriterionInput", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CreateLearningOutcomePayload", - "description": "Autogenerated return type of CreateLearningOutcome", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { "kind": "OBJECT", - "name": "ValidationError", + "name": "AssignmentGroup", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "learningOutcome", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "LearningOutcome", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CreateModuleInput", - "description": "Autogenerated input type of CreateModule", - "fields": null, - "inputFields": [ - { - "name": "name", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "courseId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CreateModulePayload", - "description": "Autogenerated return type of CreateModule", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", - "name": "ValidationError", + "name": "PageInfo", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "module", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Module", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CreateOutcomeCalculationMethodInput", - "description": "Autogenerated input type of CreateOutcomeCalculationMethod", - "fields": null, - "inputFields": [ - { - "name": "contextType", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "contextId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "calculationMethod", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "calculationInt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CreateOutcomeCalculationMethodPayload", - "description": "Autogenerated return type of CreateOutcomeCalculationMethod", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AssignmentGroupEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", + "kind": "SCALAR", + "name": "String", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcomeCalculationMethod", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "OutcomeCalculationMethod", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CreateOutcomeProficiencyInput", - "description": "Autogenerated input type of CreateOutcomeProficiency", - "fields": null, - "inputFields": [ - { - "name": "contextType", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "contextId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "AssignmentGroup", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AssignmentGroupRules", + "description": null, + "fields": [ + { + "name": "dropHighest", + "description": "The highest N assignments are not included in grade calculations", + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Int", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "proficiencyRatings", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "dropLowest", + "description": "The lowest N assignments are not included in grade calculations", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "neverDrop", + "description": null, + "args": [], + "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "INPUT_OBJECT", - "name": "OutcomeProficiencyRatingCreate", + "kind": "OBJECT", + "name": "Assignment", "ofType": null } } - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CreateOutcomeProficiencyPayload", - "description": "Autogenerated return type of CreateOutcomeProficiency", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcomeProficiency", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "OutcomeProficiency", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "AssignmentGroupState", + "description": "States that Assignment Group can be in", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "available", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleted", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "AssignmentModeratedGradingUpdate", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "enabled", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CreateSubmissionCommentInput", - "description": "Autogenerated input type of CreateSubmissionComment", - "fields": null, - "inputFields": [ - { - "name": "submissionId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "finalGraderId", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "attempt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "comment", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "graderCommentsVisibleToGraders", + "description": null, + "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "fileIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - } - }, - "defaultValue": null - }, - { - "name": "mediaObjectId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "mediaObjectType", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CreateSubmissionCommentPayload", - "description": "Autogenerated return type of CreateSubmissionComment", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionComment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "SubmissionComment", - "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CreateSubmissionDraftInput", - "description": "Autogenerated input type of CreateSubmissionDraft", - "fields": null, - "inputFields": [ - { - "name": "activeSubmissionType", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "DraftableSubmissionType", + { + "name": "graderCount", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "attempt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "body", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "externalToolId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null + { + "name": "graderNamesVisibleToFinalGrader", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "fileIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - } - }, - "defaultValue": null - }, - { - "name": "ltiLaunchUrl", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "mediaId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "resourceLinkLookupUuid", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "submissionId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "gradersAnonymousToGraders", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AssignmentOverride", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "url", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CreateSubmissionDraftPayload", - "description": "Autogenerated return type of CreateSubmissionDraft", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionDraft", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "SubmissionDraft", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CreateSubmissionInput", - "description": "Autogenerated input type of CreateSubmission", - "fields": null, - "inputFields": [ - { - "name": "annotatableAttachmentId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "assignmentId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "allDay", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "body", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextModule", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "fileIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - } - }, - "defaultValue": null - }, - { - "name": "mediaId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "resourceLinkLookupUuid", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "submissionType", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "OnlineSubmissionType", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "url", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CreateSubmissionPayload", - "description": "Autogenerated return type of CreateSubmission", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submission", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Submission", - "ofType": null + { + "name": "dueAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "DateTime", - "description": "an ISO8601 formatted time string", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "DeleteAccountDomainLookupInput", - "description": "Autogenerated input type of DeleteAccountDomainLookup", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "id", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DeleteAccountDomainLookupPayload", - "description": "Autogenerated return type of DeleteAccountDomainLookup", - "fields": [ - { - "name": "accountDomainLookupId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "DeleteCommentBankItemInput", - "description": "Autogenerated input type of DeleteCommentBankItem", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "lockAt", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "set", + "description": "This object specifies what students this override applies to", + "args": [], + "type": { + "kind": "UNION", + "name": "AssignmentOverrideSet", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DeleteCommentBankItemPayload", - "description": "Autogenerated return type of DeleteCommentBankItem", - "fields": [ - { - "name": "commentBankItemId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "title", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } + { + "name": "unassignItem", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "DeleteConversationMessagesInput", - "description": "Autogenerated input type of DeleteConversationMessages", - "fields": null, - "inputFields": [ - { - "name": "ids", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - } - } + { + "name": "unlockAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DeleteConversationMessagesPayload", - "description": "Autogenerated return type of DeleteConversationMessages", - "fields": [ - { - "name": "conversationMessageIds", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AssignmentOverrideConnection", + "description": "The connection type for AssignmentOverride.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { "kind": "LIST", "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, "ofType": { "kind": "OBJECT", - "name": "ValidationError", + "name": "AssignmentOverrideEdge", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "DeleteConversationsInput", - "description": "Autogenerated input type of DeleteConversations", - "fields": null, - "inputFields": [ - { - "name": "ids", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { "kind": "LIST", "name": null, "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - } - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DeleteConversationsPayload", - "description": "Autogenerated return type of DeleteConversations", - "fields": [ - { - "name": "conversationIds", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + "kind": "OBJECT", + "name": "AssignmentOverride", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", - "name": "ValidationError", + "name": "PageInfo", "ofType": null } - } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "AssignmentOverrideCreateOrUpdate", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "dueAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "DeleteDiscussionEntryInput", - "description": "Autogenerated input type of DeleteDiscussionEntry", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "id", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DeleteDiscussionEntryPayload", - "description": "Autogenerated return type of DeleteDiscussionEntry", - "fields": [ - { - "name": "discussionEntry", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "DiscussionEntry", - "ofType": null + { + "name": "lockAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } + { + "name": "unassignItem", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "DeleteDiscussionTopicInput", - "description": "Autogenerated input type of DeleteDiscussionTopic", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "unlockAt", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DeleteDiscussionTopicPayload", - "description": "Autogenerated return type of DeleteDiscussionTopic", - "fields": [ - { - "name": "discussionTopicId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "courseId", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "DeleteOutcomeCalculationMethodInput", - "description": "Autogenerated input type of DeleteOutcomeCalculationMethod", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "courseSectionId", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DeleteOutcomeCalculationMethodPayload", - "description": "Autogenerated return type of DeleteOutcomeCalculationMethod", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcomeCalculationMethodId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "groupId", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "DeleteOutcomeLinksInput", - "description": "Autogenerated input type of DeleteOutcomeLinks", - "fields": null, - "inputFields": [ - { - "name": "ids", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - } - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DeleteOutcomeLinksPayload", - "description": "Autogenerated return type of DeleteOutcomeLinks", - "fields": [ - { - "name": "deletedOutcomeLinkIds", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "noopId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "studentIds", + "description": null, + "type": { "kind": "LIST", "name": null, "ofType": { @@ -8762,1291 +4435,864 @@ "ofType": null } } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "DeleteOutcomeProficiencyInput", - "description": "Autogenerated input type of DeleteOutcomeProficiency", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DeleteOutcomeProficiencyPayload", - "description": "Autogenerated return type of DeleteOutcomeProficiency", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcomeProficiencyId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "DeleteSubmissionDraftInput", - "description": "Autogenerated input type of DeleteSubmissionDraft", - "fields": null, - "inputFields": [ - { - "name": "submissionId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "title", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DeleteSubmissionDraftPayload", - "description": "Autogenerated return type of DeleteSubmissionDraft", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionDraftIds", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AssignmentOverrideEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Discussion", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "AssignmentOverride", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "allowRating", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "anonymousAuthor", - "description": null, - "args": [], - "type": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "UNION", + "name": "AssignmentOverrideSet", + "description": "Objects that can be assigned overridden dates", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "AdhocStudents", + "ofType": null + }, + { "kind": "OBJECT", - "name": "AnonymousUser", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "anonymousState", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assignment", - "description": null, - "args": [], - "type": { + "name": "Course", + "ofType": null + }, + { "kind": "OBJECT", - "name": "Assignment", + "name": "Group", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attachment", - "description": null, - "args": [], - "type": { + { "kind": "OBJECT", - "name": "File", + "name": "Noop", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "author", - "description": null, - "args": [ - { - "name": "courseId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "roleTypes", - "description": "Return only requested base role types", - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - } - }, - "defaultValue": null - }, - { - "name": "builtInOnly", - "description": "Only return default/built_in roles", - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { + { "kind": "OBJECT", - "name": "User", + "name": "Section", "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "availableForUser", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "AssignmentPeerReviewsUpdate", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "anonymousReviews", + "description": null, + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "canUnpublish", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "automaticReviews", + "description": null, + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "childTopics", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Discussion", - "ofType": null - } - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "count", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Int", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextName", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + { + "name": "dueAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextType", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "enabled", + "description": null, + "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "courseSections", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Section", - "ofType": null - } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "delayedPostAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "discussionEntriesConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + { + "name": "intraReviews", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AssignmentRubricAssessment", + "description": "RubricAssessments on an Assignment", + "fields": [ + { + "name": "assessmentsCount", + "description": "The count of RubricAssessments on an Assignment.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AssignmentScoreStatistic", + "description": "Statistics for an Assignment", + "fields": [ + { + "name": "count", + "description": "The number of scores for the assignment", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lowerQ", + "description": "The lower quartile score for the assignment", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null }, - { - "name": "searchTerm", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "maximum", + "description": "The maximum score for the assignment", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null }, - { - "name": "filter", - "description": null, - "type": { - "kind": "ENUM", - "name": "DiscussionFilterType", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mean", + "description": "The mean score for the assignment", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null }, - { - "name": "sortOrder", - "description": null, - "type": { - "kind": "ENUM", - "name": "DiscussionSortOrderType", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "median", + "description": "The median score for the assignment", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null }, - { - "name": "rootEntries", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "DiscussionEntryConnection", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "discussionEntryDraftsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + { + "name": "minimum", + "description": "The minimum score for the assignment", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "upperQ", + "description": "The upper quartile score for the assignment", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "AssignmentState", + "description": "States that an Assignment can be in", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "unpublished", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "published", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleted", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "duplicating", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "failed_to_duplicate", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "importing", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fail_to_import", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "migrating", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "failed_to_migrate", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcome_alignment_cloning", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "failed_to_clone_outcome_alignment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "AssignmentTargetSortField", + "description": "Field to sort by", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "title", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "due_at", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unlock_at", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lock_at", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "AssignmentTargetSortOrder", + "description": "Specify a sort order for the results", + "fields": null, + "inputFields": [ + { + "name": "direction", + "description": null, + "type": { + "kind": "ENUM", + "name": "OrderDirection", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "DiscussionEntryDraftConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "discussionType", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "editor", - "description": null, - "args": [ - { - "name": "courseId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "field", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "AssignmentTargetSortField", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "roleTypes", - "description": "Return only requested base role types", - "type": { - "kind": "LIST", + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "AssignmentUpdate", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "abGuid", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", "name": null, "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null } - }, - "defaultValue": null + } }, - { - "name": "builtInOnly", - "description": "Only return default/built_in roles", - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "entriesTotalPages", - "description": null, - "args": [ - { - "name": "perPage", - "description": null, - "type": { + { + "name": "assignmentGroupId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentOverrides", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "Int", + "kind": "INPUT_OBJECT", + "name": "AssignmentOverrideCreateOrUpdate", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "searchTerm", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "filter", - "description": null, - "type": { - "kind": "ENUM", - "name": "DiscussionFilterType", - "ofType": null - }, - "defaultValue": null + } }, - { - "name": "sortOrder", - "description": null, - "type": { - "kind": "ENUM", - "name": "DiscussionSortOrderType", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dueAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null }, - { - "name": "rootEntries", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "entryCounts", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "DiscussionEntryCounts", - "ofType": null + { + "name": "forCheckpoints", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "groupSet", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "GroupSet", - "ofType": null + { + "name": "gradingStandardId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingType", + "description": null, + "type": { + "kind": "ENUM", + "name": "GradingType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "groupCategoryId", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "initialPostRequiredForCurrentUser", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "isAnnouncement", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "isSectionSpecific", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lastReplyAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lockAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "locked", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mentionableUsersConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "importantDates", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "intraReviews", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lockAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null }, - { - "name": "searchTerm", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "MessageableUserConnection", - "ofType": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "message", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + { + "name": "onlyVisibleToOverrides", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "modules", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Module", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "onlyGradersCanRate", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "permissions", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "DiscussionPermissions", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "podcastHasStudentPosts", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "position", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "postedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "published", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "requireInitialPost", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rootEntriesTotalPages", - "description": null, - "args": [ - { - "name": "perPage", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "searchTerm", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "filter", - "description": null, - "type": { - "kind": "ENUM", - "name": "DiscussionFilterType", - "ofType": null - }, - "defaultValue": null + { + "name": "peerReviews", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignmentPeerReviewsUpdate", + "ofType": null }, - { - "name": "sortOrder", - "description": null, - "type": { - "kind": "ENUM", - "name": "DiscussionSortOrderType", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rootTopic", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Discussion", - "ofType": null + { + "name": "pointsPossible", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "searchEntryCount", - "description": null, - "args": [ - { - "name": "searchTerm", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + { + "name": "postToSis", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "filter", - "description": null, - "type": { - "kind": "ENUM", - "name": "DiscussionFilterType", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sortByRating", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "subscribed", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "title", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "ModuleItemInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DiscussionEntry", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unlockAt", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "anonymousAuthor", - "description": null, - "args": [], - "type": { + { + "name": "setAssignment", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INTERFACE", + "name": "AssignmentsConnectionInterface", + "description": null, + "fields": [ + { + "name": "assignmentsConnection", + "description": "returns a list of assignments.\n\n**NOTE**: for courses with grading periods, this will only return grading\nperiods in the current course; see `AssignmentFilter` for more info.\nIn courses with grading periods that don't have students, it is necessary\nto *not* filter by grading period to list assignments.\n", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignmentFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "AssignmentConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { "kind": "OBJECT", - "name": "AnonymousUser", + "name": "AssignmentGroup", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attachment", - "description": null, - "args": [], - "type": { + { "kind": "OBJECT", - "name": "File", + "name": "Course", "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "author", - "description": null, - "args": [ - { - "name": "courseId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "roleTypes", - "description": "Return only requested base role types", - "type": { - "kind": "LIST", - "name": null, - "ofType": { + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "AuditLogs", + "description": null, + "fields": [ + { + "name": "mutationLogs", + "description": "A list of all recent graphql mutations run on the specified object", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assetString", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { @@ -10054,1683 +5300,864 @@ "name": "String", "ofType": null } - } - }, - "defaultValue": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "endTime", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "startTime", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "MutationLogConnection", + "ofType": null }, - { - "name": "builtInOnly", - "description": "Only return default/built_in roles", - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "AutoLeaderPolicy", + "description": "Determines if/how a leader is chosen for each group", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "random", + "description": "a leader is chosen at random", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "the first student assigned to the group is the leader", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "AutoLeaderType", + "description": "Method of selecting an automatic leader for groups", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "first", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "random", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "Represents `true` or `false` values.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Checkpoint", + "description": null, + "fields": [ + { + "name": "assignmentOverrides", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "AssignmentOverrideConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dueAt", + "description": "when this checkpoint is due", + "args": [ + { + "name": "applyOverrides", + "description": "When true, return the overridden dates.\n\nNot all roles have permission to view un-overridden dates (in which\ncase the overridden dates will be returned)\n", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "true", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lockAt", + "description": "when this checkpoint is closed", + "args": [ + { + "name": "applyOverrides", + "description": "When true, return the overridden dates.\n\nNot all roles have permission to view un-overridden dates (in which\ncase the overridden dates will be returned)\n", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "true", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleted", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "discussionSubentriesConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onlyVisibleToOverrides", + "description": "specifies that this checkpoint is only assigned to students for whom an override applies", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": "the checkpoint is out of this many points", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "Float", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tag", + "description": "the tag of the checkpoint", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "String", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "sortOrder", - "description": null, - "type": { - "kind": "ENUM", - "name": "DiscussionSortOrderType", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unlockAt", + "description": "when this checkpoint is available", + "args": [ + { + "name": "applyOverrides", + "description": "When true, return the overridden dates.\n\nNot all roles have permission to view un-overridden dates (in which\ncase the overridden dates will be returned)\n", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "true", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null }, - { - "name": "relativeEntryId", - "description": null, - "type": { + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "CheckpointLabelType", + "description": "Valid labels for discussion checkpoint types", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "reply_to_topic", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reply_to_entry", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CommentBankItem", + "description": "Comment bank items", + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "beforeRelativeEntry", - "description": null, - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "comment", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "includeRelativeEntry", - "description": null, - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Boolean", + "name": "ID", "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "DiscussionEntryConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "discussionTopic", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Discussion", - "ofType": null - } + } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "discussionTopicId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "createdAt", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "editor", - "description": null, - "args": [ - { - "name": "courseId", - "description": null, - "type": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null - }, - "defaultValue": null - }, - { - "name": "roleTypes", - "description": "Return only requested base role types", - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - } - }, - "defaultValue": null + } }, - { - "name": "builtInOnly", - "description": "Only return default/built_in roles", - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "entryParticipant", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "EntryParticipant", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "isolatedEntryId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lastReply", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "DiscussionEntry", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "message", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "parentId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "permissions", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "DiscussionEntryPermissions", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "previewMessage", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "quotedEntry", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "DiscussionEntry", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ratingCount", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ratingSum", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rootEntry", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "DiscussionEntry", + { + "kind": "INTERFACE", + "name": "Node", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rootEntryId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rootEntryParticipantCounts", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "DiscussionEntryCounts", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "subentriesCount", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DiscussionEntryConnection", - "description": "The connection type for DiscussionEntry.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "DiscussionEntryEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CommentBankItemConnection", + "description": "The connection type for CommentBankItem.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CommentBankItemEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CommentBankItem", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CommentBankItemEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { "kind": "OBJECT", - "name": "DiscussionEntry", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DiscussionEntryCounts", - "description": null, - "fields": [ - { - "name": "deletedCount", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "repliesCount", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unreadCount", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", + "name": "CommentBankItem", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CommunicationChannel", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DiscussionEntryDraft", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "createdAt", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attachment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "File", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notificationPolicies", + "description": null, + "args": [ + { + "name": "contextType", + "description": null, + "type": { + "kind": "ENUM", + "name": "NotificationPreferencesContextType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "NotificationPolicy", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notificationPolicyOverrides", + "description": null, + "args": [ + { + "name": "accountId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "NotificationPreferencesContextType", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "NotificationPolicy", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "discussionEntryId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "discussionTopicId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "path", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "pathType", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "includeReplyPreview", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "message", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "parentId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rootEntryId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DiscussionEntryDraftConnection", - "description": "The connection type for DiscussionEntryDraft.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "DiscussionEntryDraftEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "DiscussionEntryDraft", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DiscussionEntryDraftEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "DiscussionEntryDraft", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DiscussionEntryEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "DiscussionEntry", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DiscussionEntryPermissions", - "description": null, - "fields": [ - { - "name": "attach", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "create", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "delete", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rate", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "read", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "reply", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "update", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "viewRating", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "DiscussionFilterType", - "description": "Search types that can be associated with discussions", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "all", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unread", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "drafts", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleted", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DiscussionPermissions", - "description": null, - "fields": [ - { - "name": "addRubric", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attach", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "closeForComments", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "copyAndSendTo", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "create", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "delete", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "duplicate", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "manageContent", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "moderateForum", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "openForComments", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "peerReview", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rate", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "read", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "readAsAdmin", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "readReplies", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "reply", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "showRubric", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "speedGrader", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "studentReporting", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "update", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "DiscussionSortOrderType", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "asc", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "desc", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "DraftableSubmissionType", - "description": "Types of submissions that can have a submission draft", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "basic_lti_launch", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "media_recording", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "online_text_entry", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "online_upload", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "online_url", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "student_annotation", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Enrollment", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assetString", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "associatedUser", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "User", + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "course", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Course", + { + "kind": "INTERFACE", + "name": "Node", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "grades", - "description": null, - "args": [ - { - "name": "gradingPeriodId", - "description": "The grading period to return grades for. If not specified, will use the current grading period (or the course grade for courses that don't use grading periods)", - "type": { + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ContentTag", + "description": "An edge in a connection.", + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "Grades", - "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "canUnlink", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lastActivityAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "section", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Section", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "state", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "EnrollmentWorkflowState", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "type", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "EnrollmentType", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "user", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "AssetString", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "EnrollmentConnection", - "description": "The connection type for Enrollment.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "EnrollmentEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Enrollment", + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "EnrollmentEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "Enrollment", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "EnrollmentFilterInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "types", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "ENUM", - "name": "EnrollmentType", + "kind": "SCALAR", + "name": "String", "ofType": null } - } - }, - "defaultValue": "null" - }, - { - "name": "associatedUserIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "group", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "LearningOutcomeGroup", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { @@ -11738,1379 +6165,393 @@ "name": "ID", "ofType": null } - } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "UNION", + "name": "ContentTagContent", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": "[]" - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "EnrollmentType", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "StudentEnrollment", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "TeacherEnrollment", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "TaEnrollment", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ObserverEnrollment", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "DesignerEnrollment", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "StudentViewEnrollment", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "EnrollmentWorkflowState", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "invited", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "creation_pending", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "active", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleted", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rejected", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "completed", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inactive", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "EntryParticipant", - "description": null, - "fields": [ - { - "name": "forcedReadState", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rating", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "read", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "reportType", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ExternalTool", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "modules", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ContentTagConnection", + "description": "The connection type for ContentTagContent.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ContentTag", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "UNION", + "name": "ContentTagContent", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", - "name": "Module", + "name": "PageInfo", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "settings", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "ExternalToolSettings", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "state", - "description": null, - "args": [], - "type": { - "kind": "ENUM", - "name": "ExternalToolState", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "url", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "ModuleItemInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ExternalToolConnection", - "description": "The connection type for ExternalTool.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ExternalToolEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ExternalTool", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ExternalToolEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "ExternalTool", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "ExternalToolFilterInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "state", - "description": null, - "type": { - "kind": "ENUM", - "name": "ExternalToolState", - "ofType": null - }, - "defaultValue": "null" - }, - { - "name": "placement", - "description": null, - "type": { - "kind": "ENUM", - "name": "ExternalToolPlacement", - "ofType": null - }, - "defaultValue": "null" - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "ExternalToolPlacement", - "description": "Placements that an External Tool can have", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "homework_submission", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ExternalToolPlacements", - "description": null, - "fields": [ - { - "name": "canvasIconClass", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "iconUrl", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "messageType", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "text", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "url", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ExternalToolSettings", - "description": null, - "fields": [ - { - "name": "homeworkSubmission", - "description": null, - "args": [], - "type": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "UNION", + "name": "ContentTagContent", + "description": "Content of a Content Tag", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { "kind": "OBJECT", - "name": "ExternalToolPlacements", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "iconUrl", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "selectionHeight", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "selectionWidth", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "text", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "ExternalToolState", - "description": "States that an External Tool can be in", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "anonymous", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name_only", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "email_only", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "public", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ExternalUrl", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", + "name": "LearningOutcome", "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "modules", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Conversation", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "Module", + "kind": "SCALAR", + "name": "ID", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "title", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "url", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "ModuleItemInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "File", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canReply", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contentType", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "displayName", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextAssetString", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextId", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mimeClass", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + { + "name": "contextName", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "modules", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + { + "name": "contextType", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "conversationMessagesConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdBefore", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "participants", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ConversationMessageConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "conversationMessagesCount", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "Module", + "kind": "SCALAR", + "name": "Int", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionPreviewUrl", - "description": null, - "args": [ - { - "name": "submissionId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "conversationParticipantsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "thumbnailUrl", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "url", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "ModuleItemInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "FileConnection", - "description": "The connection type for File.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "FileEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "File", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "FileEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "File", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "Float", - "description": "Represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "GradeState", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "active", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleted", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Grades", - "description": "Contains grade information for a course or grading period", - "fields": [ - { - "name": "assignmentGroup", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "AssignmentGroup", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "currentGrade", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "currentScore", - "description": "The current score includes all graded assignments, excluding muted submissions.\n", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "enrollment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Enrollment", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "finalGrade", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "finalScore", - "description": "The final score includes all assignments, excluding muted submissions\n(ungraded assignments are counted as 0 points).\n", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradingPeriod", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "GradingPeriod", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "overrideGrade", - "description": "The override grade. Supersedes the computed final grade if set.\n", - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "overrideScore", - "description": "The override score. Supersedes the computed final score if set.\n", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "state", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "GradeState", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unpostedCurrentGrade", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unpostedCurrentScore", - "description": "The current score includes all graded assignments, including muted submissions.\n", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unpostedFinalGrade", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unpostedFinalScore", - "description": "The final score includes all assignments, including muted submissions\n(ungraded assignments are counted as 0 points).\n", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "GradesConnection", - "description": "The connection type for Grades.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "GradesEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "OBJECT", - "name": "Grades", + "name": "ConversationParticipantConnection", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "GradesEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "Grades", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "GradesEnrollmentFilter", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "enrollmentIds", - "description": "only include users with the given enrollment ids", - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { @@ -13118,1288 +6559,390 @@ "name": "ID", "ofType": null } - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "GradingPeriod", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "isPrivate", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "closeDate", - "description": "assignments can only be graded before the grading period closes\n", - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "endDate", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "startDate", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "title", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "weight", - "description": "used to calculate how much the assignments in this grading period\ncontribute to the overall grade\n", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "GradingPeriodConnection", - "description": "The connection type for GradingPeriod.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "GradingPeriodEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "GradingPeriod", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "GradingPeriodEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "GradingPeriod", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "GradingType", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "points", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "percent", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "letter_grade", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gpa_scale", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pass_fail", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "not_graded", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Group", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "subject", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assetString", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "member", - "description": null, - "args": [ - { - "name": "userId", - "description": null, - "type": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ConversationMessage", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attachments", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "ID", + "kind": "OBJECT", + "name": "File", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "GroupMembership", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "membersConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "GroupMembershipConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sisId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "AssetString", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "GroupConnection", - "description": "The connection type for Group.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "GroupEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attachmentsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "OBJECT", - "name": "Group", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "GroupEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "Group", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "GroupMembership", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + "name": "FileConnection", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "state", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "GroupMembershipState", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "user", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "GroupMembershipConnection", - "description": "The connection type for GroupMembership.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "GroupMembershipEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "author", + "description": null, + "args": [], + "type": { "kind": "OBJECT", - "name": "GroupMembership", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "GroupMembershipEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "GroupMembership", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "GroupMembershipState", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "accepted", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "invited", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "requested", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rejected", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleted", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "GroupSet", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + "name": "User", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "autoLeader", - "description": null, - "args": [], - "type": { - "kind": "ENUM", - "name": "AutoLeaderPolicy", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "groupsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "body", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "conversationId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "GroupConnection", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "createdAt", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "memberLimit", - "description": "Sets a cap on the number of members in the group. Only applies when\nself-signup is enabled.\n", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "selfSignup", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "SelfSignupPolicy", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sisId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "GroupSetConnection", - "description": "The connection type for GroupSet.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "GroupSetEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaComment", + "description": null, + "args": [], + "type": { "kind": "OBJECT", - "name": "GroupSet", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "GroupSetEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "GroupSet", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "HideAssignmentGradesForSectionsInput", - "description": "Autogenerated input type of HideAssignmentGradesForSections", - "fields": null, - "inputFields": [ - { - "name": "assignmentId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + "name": "MediaObject", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "sectionIds", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "recipients", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", "name": null, "ofType": { - "kind": "NON_NULL", + "kind": "LIST", "name": null, "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } } } - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "HideAssignmentGradesForSectionsPayload", - "description": "Autogenerated return type of HideAssignmentGradesForSections", - "fields": [ - { - "name": "assignment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ConversationMessageConnection", + "description": "The connection type for ConversationMessage.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { "kind": "OBJECT", - "name": "ValidationError", + "name": "ConversationMessageEdge", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "progress", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Progress", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sections", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { "kind": "OBJECT", - "name": "Section", + "name": "ConversationMessage", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "HideAssignmentGradesInput", - "description": "Autogenerated input type of HideAssignmentGrades", - "fields": null, - "inputFields": [ - { - "name": "assignmentId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "sectionIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "ID", + "kind": "OBJECT", + "name": "PageInfo", "ofType": null } - } - }, - "defaultValue": null - }, - { - "name": "onlyStudentIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ConversationMessageEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null } - } - }, - "defaultValue": null - }, - { - "name": "skipStudentIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "ConversationMessage", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ConversationParticipant", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { @@ -14407,6893 +6950,38746 @@ "name": "ID", "ofType": null } - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "HideAssignmentGradesPayload", - "description": "Autogenerated return type of HideAssignmentGrades", - "fields": [ - { - "name": "assignment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "progress", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Progress", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sections", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Section", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "ID", - "description": "Represents a unique identifier that is Base64 obfuscated. It is often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"VXNlci0xMA==\"`) or integer (such as `4`) input value will be accepted as an ID.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "ImportOutcomesInput", - "description": "Autogenerated input type of ImportOutcomes", - "fields": null, - "inputFields": [ - { - "name": "groupId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "outcomeId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "sourceContextId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "sourceContextType", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "targetGroupId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "targetContextId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "targetContextType", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ImportOutcomesPayload", - "description": "Autogenerated return type of ImportOutcomes", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "conversation", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Conversation", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", + "kind": "SCALAR", + "name": "ID", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "progress", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Progress", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "Int", - "description": "Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "JSON", - "description": "Represents untyped JSON", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "LatePolicyStatusType", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "late", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "missing", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "none", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "LearningOutcome", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "label", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assessed", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "calculationInt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "calculationMethod", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "canEdit", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextType", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "displayName", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "friendlyDescription", - "description": null, - "args": [ - { - "name": "contextId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "messages", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "contextType", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { "kind": "SCALAR", "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "OutcomeFriendlyDescriptionType", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "isImported", - "description": null, - "args": [ - { - "name": "targetContextId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "targetContextType", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { "kind": "SCALAR", - "name": "String", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rubricCriterion", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "RubricCriterion", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "title", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "vendorGuid", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "LearningOutcomeGroup", - "description": "Learning Outcome Group", - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "canEdit", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ConversationMessageConnection", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "childGroups", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscribed", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "Boolean", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "LearningOutcomeGroupConnection", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "childGroupsCount", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "Int", + "name": "DateTime", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextType", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "notImportedOutcomesCount", - "description": null, - "args": [ - { - "name": "targetGroupId", - "description": null, - "type": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcomes", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "workflowState", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ConversationParticipantConnection", + "description": "The connection type for ConversationParticipant.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ConversationParticipantEdge", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ConversationParticipant", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "searchQuery", - "description": null, - "type": { + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ConversationParticipantEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { "kind": "OBJECT", - "name": "ContentTagConnection", + "name": "ConversationParticipant", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcomesCount", - "description": null, - "args": [ - { - "name": "searchQuery", - "description": null, - "type": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Course", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "account", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Account", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "parentOutcomeGroup", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "LearningOutcomeGroup", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "title", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "vendorGuid", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "LearningOutcomeGroupConnection", - "description": "The connection type for LearningOutcomeGroup.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "LearningOutcomeGroupEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "activityStream", + "description": null, + "args": [], + "type": { "kind": "OBJECT", - "name": "LearningOutcomeGroup", + "name": "ActivityStream", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "LearningOutcomeGroupEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "LearningOutcomeGroup", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "allowFinalGradeOverride", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": [ - { - "kind": "OBJECT", - "name": "Account", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "AccountDomain", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "AccountDomainLookup", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "AssessmentRequest", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "AssignmentGroup", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "AssignmentOverride", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "CommentBankItem", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "CommunicationChannel", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ContentTag", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Course", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Discussion", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "DiscussionEntry", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "DiscussionEntryDraft", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Enrollment", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ExternalTool", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ExternalUrl", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "File", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "GradingPeriod", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Group", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "GroupMembership", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "GroupSet", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "LearningOutcome", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "LearningOutcomeGroup", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "MediaTrack", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Module", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ModuleExternalTool", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ModuleItem", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Notification", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "NotificationPolicy", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "OutcomeCalculationMethod", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "OutcomeFriendlyDescriptionType", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "OutcomeProficiency", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Page", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "PostPolicy", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ProficiencyRating", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Progress", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Quiz", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Rubric", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "RubricAssessment", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "RubricAssociation", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "RubricCriterion", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "RubricRating", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Section", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Submission", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "SubmissionComment", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "SubmissionDraft", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Term", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "User", - "ofType": null - } - ] - }, - { - "kind": "OBJECT", - "name": "LockInfo", - "description": null, - "fields": [ - { - "name": "canView", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "isLocked", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lockAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lockedObject", - "description": null, - "args": [], - "type": { - "kind": "UNION", - "name": "Lockable", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "module", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Module", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unlockAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "UNION", - "name": "Lockable", - "description": "Types that can be locked", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": [ - { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Discussion", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Module", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Page", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Quiz", - "ofType": null - } - ] - }, - { - "kind": "INPUT_OBJECT", - "name": "MarkSubmissionCommentsReadInput", - "description": "Autogenerated input type of MarkSubmissionCommentsRead", - "fields": null, - "inputFields": [ - { - "name": "submissionCommentIds", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "applyGroupWeights", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assetString", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentGroups", + "description": null, + "args": [], + "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "ID", + "kind": "OBJECT", + "name": "AssignmentGroup", "ofType": null } } - } - }, - "defaultValue": null - }, - { - "name": "submissionId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MarkSubmissionCommentsReadPayload", - "description": "Autogenerated return type of MarkSubmissionCommentsRead", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionComments", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "SubmissionComment", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentGroupsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MediaObject", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "canAddCaptions", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + ], + "type": { + "kind": "OBJECT", + "name": "AssignmentGroupConnection", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mediaSources", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "MediaSource", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentPostPolicies", + "description": "PostPolicies for assignments within a course\n", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mediaTracks", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "MediaTrack", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mediaType", - "description": null, - "args": [], - "type": { - "kind": "ENUM", - "name": "MediaType", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "title", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MediaSource", - "description": null, - "fields": [ - { - "name": "bitrate", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contentType", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "fileExt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "height", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "isOriginal", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "size", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "url", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "width", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MediaTrack", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + ], + "type": { + "kind": "OBJECT", + "name": "PostPolicyConnection", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "content", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "kind", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "locale", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mediaObject", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "MediaObject", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "webvttContent", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "MediaType", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "audio", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "video", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MessagePermissions", - "description": null, - "fields": [ - { - "name": "sendMessages", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sendMessagesAll", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MessageableContext", - "description": null, - "fields": [ - { - "name": "avatarUrl", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentsConnection", + "description": "returns a list of assignments.\n\n**NOTE**: for courses with grading periods, this will only return grading\nperiods in the current course; see `AssignmentFilter` for more info.\nIn courses with grading periods that don't have students, it is necessary\nto *not* filter by grading period to list assignments.\n", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignmentFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "AssignmentConnection", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "itemCount", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "permissions", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "MessagePermissions", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "userCount", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MessageableContextConnection", - "description": "The connection type for MessageableContext.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "MessageableContextEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "availableModerators", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "OBJECT", - "name": "MessageableContext", + "name": "UserConnection", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MessageableContextEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "MessageableContext", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MessageableUser", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "availableModeratorsCount", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Int", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "commonCoursesConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "EnrollmentConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "commonGroupsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "GroupConnection", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "courseCode", + "description": "course short name", + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MessageableUserConnection", - "description": "The connection type for MessageableUser.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "MessageableUserEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "MessageableUser", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MessageableUserEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "MessageableUser", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ModeratedGrading", - "description": "Settings for Moderated Grading on an Assignment", - "fields": [ - { - "name": "enabled", - "description": "Boolean indicating if the assignment is moderated.", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "finalGrader", - "description": "The user of the grader responsible for choosing final grades for this assignment.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "graderCommentsVisibleToGraders", - "description": "Boolean indicating if provisional graders' comments are visible to other provisional graders.", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "graderCount", - "description": "The maximum number of provisional graders who may issue grades for this assignment.", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "graderNamesVisibleToFinalGrader", - "description": "Boolean indicating if provisional graders' identities are hidden from other provisional graders.", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradersAnonymousToGraders", - "description": "Boolean indicating if provisional grader identities are visible to the final grader.", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Module", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "courseNickname", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "moduleItems", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ModuleItem", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customGradeStatusesConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "position", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unlockAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ModuleConnection", - "description": "The connection type for Module.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ModuleEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + ], + "type": { "kind": "OBJECT", - "name": "Module", + "name": "CustomGradeStatusConnection", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ModuleEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "Module", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ModuleExternalTool", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dashboardCard", + "description": "returns dashboard card information for this course", + "args": [], + "type": { + "kind": "OBJECT", + "name": "CourseDashboardCard", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "modules", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Module", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussionsConnection", + "description": "returns a list of discussions.\n", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "DiscussionFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "url", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "ModuleItemInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ModuleItem", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "content", - "description": null, - "args": [], - "type": { - "kind": "INTERFACE", - "name": "ModuleItemInterface", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + ], + "type": { + "kind": "OBJECT", + "name": "DiscussionConnection", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "module", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Module", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "next", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "ModuleItem", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "previous", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "ModuleItem", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "url", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INTERFACE", - "name": "ModuleItemInterface", - "description": "An item that can be in context modules", - "fields": [ - { - "name": "modules", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enrollmentsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "EnrollmentFilterInput", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "EnrollmentConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "externalToolsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "ExternalToolFilterInput", + "ofType": null + }, + "defaultValue": "{}", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ExternalToolConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filesConnection", + "description": "returns a list of files.\n", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "FileFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "FileConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradeStatuses", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "Module", - "ofType": null + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "CourseGradeStatus", + "ofType": null + } + } } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": [ - { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Discussion", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ExternalTool", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ExternalUrl", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "File", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ModuleExternalTool", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Page", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Quiz", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "SubHeader", - "ofType": null - } - ] - }, - { - "kind": "INPUT_OBJECT", - "name": "MoveOutcomeLinksInput", - "description": "Autogenerated input type of MoveOutcomeLinks", - "fields": null, - "inputFields": [ - { - "name": "outcomeLinkIds", - "description": "A list of ContentTags that will be moved\n", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingPeriodsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "GradingPeriodConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingStandard", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "GradingStandard", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupSets", + "description": "Project group sets for this course.", + "args": [ + { + "name": "includeNonCollaborative", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "ID", + "kind": "OBJECT", + "name": "GroupSet", "ofType": null } } - } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupSetsConnection", + "description": "Project group sets for this course.", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "includeNonCollaborative", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "GroupSetConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "includeNonCollaborative", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "GroupConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "groupId", - "description": "The id of the destination group\n", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "horizonCourse", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MoveOutcomeLinksPayload", - "description": "Autogenerated return type of MoveOutcomeLinks", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + { + "name": "horizonCourse", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", + "kind": "SCALAR", + "name": "ID", "ofType": null } - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "movedOutcomeLinks", - "description": "List of Outcome Links that were sucessfully moved to the group\n", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", + { + "name": "imageUrl", + "description": "Returns a URL for the course image (this is the image used on dashboard\ncourse cards)\n", + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "modulesConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ModuleConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", "name": null, "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ContentTag", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Mutation", - "description": null, - "fields": [ - { - "name": "addConversationMessage", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "AddConversationMessageInput", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeAlignmentStats", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "CourseOutcomeAlignmentStats", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeCalculationMethod", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "OutcomeCalculationMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeProficiency", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "OutcomeProficiency", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pagesConnection", + "description": "returns a list of wiki pages.\n", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "AddConversationMessagePayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createAccountDomainLookup", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "CreateAccountDomainLookupInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CreateAccountDomainLookupPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createAssignment", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "CreateAssignmentInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CreateAssignmentPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createCommentBankItem", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "CreateCommentBankItemInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CreateCommentBankItemPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createConversation", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { "kind": "INPUT_OBJECT", - "name": "CreateConversationInput", + "name": "PageFilter", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CreateConversationPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createDiscussionEntry", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "CreateDiscussionEntryInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PageConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "permissions", + "description": "returns permission information for the current user in this course", + "args": [], + "type": { + "kind": "OBJECT", + "name": "CoursePermissions", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postPolicy", + "description": "A course-specific post policy", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PostPolicy", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "quizzesConnection", + "description": "returns a list of quizzes.\n", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CreateDiscussionEntryPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createDiscussionEntryDraft", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "CreateDiscussionEntryDraftInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CreateDiscussionEntryDraftPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createGroupInSet", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "CreateGroupInSetInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CreateGroupInSetPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createLearningOutcome", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "CreateLearningOutcomeInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CreateLearningOutcomePayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createLearningOutcomeGroup", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { "kind": "INPUT_OBJECT", - "name": "CreateLearningOutcomeGroupInput", + "name": "QuizFilter", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CreateLearningOutcomeGroupPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createModule", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "CreateModuleInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "QuizConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "relevantGradingPeriodGroup", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "GradingPeriodGroup", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rootOutcomeGroup", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "LearningOutcomeGroup", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubricsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CreateModulePayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createOutcomeCalculationMethod", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "CreateOutcomeCalculationMethodInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CreateOutcomeCalculationMethodPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createOutcomeProficiency", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "CreateOutcomeProficiencyInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CreateOutcomeProficiencyPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createSubmission", - "description": "IN ACTIVE DEVELOPMENT, USE AT YOUR OWN RISK: Submit homework on an assignment.\n", - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "CreateSubmissionInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CreateSubmissionPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createSubmissionComment", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "CreateSubmissionCommentInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RubricConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sectionsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CreateSubmissionCommentPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createSubmissionDraft", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "CreateSubmissionDraftInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CreateSubmissionDraftPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleteAccountDomainLookup", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "DeleteAccountDomainLookupInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "DeleteAccountDomainLookupPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleteCommentBankItem", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "DeleteCommentBankItemInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "DeleteCommentBankItemPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleteConversationMessages", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { "kind": "INPUT_OBJECT", - "name": "DeleteConversationMessagesInput", + "name": "CourseSectionsFilter", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "DeleteConversationMessagesPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleteConversations", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "DeleteConversationsInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SectionConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sisId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "CourseWorkflowState", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionsConnection", + "description": "all the submissions for assignments in this course", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "DeleteConversationsPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleteDiscussionEntry", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "DeleteDiscussionEntryInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "DeleteDiscussionEntryPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleteDiscussionTopic", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "DeleteDiscussionTopicInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "DeleteDiscussionTopicPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleteOutcomeCalculationMethod", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "DeleteOutcomeCalculationMethodInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "DeleteOutcomeCalculationMethodPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleteOutcomeLinks", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { "kind": "INPUT_OBJECT", - "name": "DeleteOutcomeLinksInput", + "name": "SubmissionFilterInput", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "DeleteOutcomeLinksPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleteOutcomeProficiency", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "DeleteOutcomeProficiencyInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "orderBy", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SubmissionOrderCriteria", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "studentIds", + "description": "Only return submissions for the given students.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SubmissionConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "syllabusBody", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "term", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Term", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "usersConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "DeleteOutcomeProficiencyPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleteSubmissionDraft", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "DeleteSubmissionDraftInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "DeleteSubmissionDraftPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "hideAssignmentGrades", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "HideAssignmentGradesInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "HideAssignmentGradesPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "hideAssignmentGradesForSections", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "HideAssignmentGradesForSectionsInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "HideAssignmentGradesForSectionsPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "importOutcomes", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userIds", + "description": "Only include users with the given ids.\n\n**This field is deprecated, use `filter: {userIds}` instead.**\n", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { "kind": "INPUT_OBJECT", - "name": "ImportOutcomesInput", + "name": "CourseUsersFilter", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "ImportOutcomesPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "markSubmissionCommentsRead", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UserConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "AssetString", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "AssignmentsConnectionInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "DiscussionsConnectionInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "FilesConnectionInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "DiscussionsConnectionInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "FilesConnectionInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "PagesConnectionInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "QuizzesConnectionInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "PagesConnectionInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "QuizzesConnectionInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CourseConnection", + "description": "The connection type for Course.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CourseEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Course", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CourseDashboardCard", + "description": "A card on the course dashboard", + "fields": [ + { + "name": "assetString", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canChangeCoursePublishState", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canManage", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canReadAnnouncements", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "color", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseCode", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultView", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enrollmentState", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enrollmentType", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "frontPageTitle", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "href", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "image", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isFavorited", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isHomeroom", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isK5Subject", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "links", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CourseDashboardCardLink", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "longName", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "observee", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "originalName", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pagesUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "position", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "published", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "shortName", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subtitle", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "term", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Term", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "useClassicFont", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CourseDashboardCardLink", + "description": "A link on a course dashboard card", + "fields": [ + { + "name": "cssClass", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hidden", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "icon", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "label", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "path", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CourseEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Course", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "CourseFilterableEnrollmentState", + "description": "Users in a course can be returned based on these enrollment states", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "invited", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "creation_pending", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "active", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rejected", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "completed", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inactive", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "CourseFilterableEnrollmentType", + "description": "Users in a course can be returned based on these enrollment types", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "StudentEnrollment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TeacherEnrollment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TaEnrollment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ObserverEnrollment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DesignerEnrollment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "StudentViewEnrollment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "CourseGradeStatus", + "description": "Grade statuses that can be applied to submissions in a course", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "late", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "missing", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "none", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "excused", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "extended", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CourseOutcomeAlignmentStats", + "description": null, + "fields": [ + { + "name": "alignedArtifacts", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "alignedOutcomes", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "artifactAlignments", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalAlignments", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalArtifacts", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalOutcomes", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CoursePermissions", + "description": null, + "fields": [ + { + "name": "becomeUser", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "manageGrades", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sendMessages", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "viewAllGrades", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "viewAnalytics", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CourseProgression", + "description": null, + "fields": [ + { + "name": "incompleteModulesConnection", + "description": "Modules are ordered by position", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ModuleProgressionConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requirements", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CourseRequirements", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CourseRequirements", + "description": null, + "fields": [ + { + "name": "completed", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "completionPercentage", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "total", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CourseSectionsFilter", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "assignmentId", + "description": "Only include sections associated with users assigned to this assignment", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CourseUsersFilter", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "userIds", + "description": "only include users with the given ids", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enrollmentRoleIds", + "description": "Only return users with the specified enrollment role ids", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enrollmentStates", + "description": "only return users with the given enrollment state. defaults\nto `invited`, `creation_pending`, `active`\n", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "CourseFilterableEnrollmentState", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enrollmentTypes", + "description": "Only return users with the specified enrollment types", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "CourseFilterableEnrollmentType", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "excludeTestStudents", + "description": "Exclude test students from results", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "searchTerm", + "description": "Only return users that match the given search term. The search\nterm is matched against the user's name and depending on current\nuser permissions against the user's login id, email and sisid\n", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "CourseWorkflowState", + "description": "States that Courses can be in", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "created", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "claimed", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "available", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "completed", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleted", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateAccountDomainLookupInput", + "description": "Autogenerated input type of CreateAccountDomainLookup", + "fields": null, + "inputFields": [ + { + "name": "accountDomainId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authenticationProvider", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateAccountDomainLookupPayload", + "description": "Autogenerated return type of CreateAccountDomainLookup.", + "fields": [ + { + "name": "accountDomainLookup", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "AccountDomainLookup", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateAssignmentInput", + "description": "Autogenerated input type of CreateAssignment", + "fields": null, + "inputFields": [ + { + "name": "allowedAttempts", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allowedExtensions", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anonymousGrading", + "description": "requires anonymous_marking course feature to be set to true", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anonymousInstructorAnnotations", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentGroupId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentOverrides", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AssignmentOverrideCreateOrUpdate", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dueAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "forCheckpoints", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradeGroupStudentsIndividually", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingStandardId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingType", + "description": null, + "type": { + "kind": "ENUM", + "name": "GradingType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupCategoryId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupSetId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lockAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "moderatedGrading", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignmentModeratedGradingUpdate", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "moduleIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "omitFromFinalGrade", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onlyVisibleToOverrides", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "peerReviews", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignmentPeerReviewsUpdate", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "position", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postToSis", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": null, + "type": { + "kind": "ENUM", + "name": "AssignmentState", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionTypes", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SubmissionType", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unlockAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateAssignmentPayload", + "description": "Autogenerated return type of CreateAssignment.", + "fields": [ + { + "name": "assignment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateCommentBankItemInput", + "description": "Autogenerated input type of CreateCommentBankItem", + "fields": null, + "inputFields": [ + { + "name": "comment", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateCommentBankItemPayload", + "description": "Autogenerated return type of CreateCommentBankItem.", + "fields": [ + { + "name": "commentBankItem", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "CommentBankItem", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateConversationInput", + "description": "Autogenerated input type of CreateConversation", + "fields": null, + "inputFields": [ + { + "name": "attachmentIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "body", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bulkMessage", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextCode", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "conversationId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "forceNew", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupConversation", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaCommentId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaCommentType", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "recipients", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subject", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tags", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateConversationPayload", + "description": "Autogenerated return type of CreateConversation.", + "fields": [ + { + "name": "conversations", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ConversationParticipant", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateDiscussionEntryDraftInput", + "description": "Autogenerated input type of CreateDiscussionEntryDraft", + "fields": null, + "inputFields": [ + { + "name": "discussionEntryId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussionTopicId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fileId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "parentId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateDiscussionEntryDraftPayload", + "description": "Autogenerated return type of CreateDiscussionEntryDraft.", + "fields": [ + { + "name": "discussionEntryDraft", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntryDraft", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateDiscussionEntryInput", + "description": "Autogenerated input type of CreateDiscussionEntry", + "fields": null, + "inputFields": [ + { + "name": "discussionTopicId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fileId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "parentEntryId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isAnonymousAuthor", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "quotedEntryId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateDiscussionEntryPayload", + "description": "Autogenerated return type of CreateDiscussionEntry.", + "fields": [ + { + "name": "discussionEntry", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntry", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mySubAssignmentSubmissions", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Submission", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateDiscussionTopicInput", + "description": "Autogenerated input type of CreateDiscussionTopic", + "fields": null, + "inputFields": [ + { + "name": "allowRating", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "checkpoints", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DiscussionCheckpoints", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "delayedPostAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expanded", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expandedLocked", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fileId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupCategoryId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lockAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locked", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onlyGradersCanRate", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onlyVisibleToOverrides", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "podcastEnabled", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "podcastHasStudentPosts", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "published", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requireInitialPost", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": null, + "type": { + "kind": "ENUM", + "name": "DiscussionSortOrderType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrderLocked", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "specificSections", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "todoDate", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anonymousState", + "description": null, + "type": { + "kind": "ENUM", + "name": "DiscussionTopicAnonymousStateType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignment", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignmentCreate", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "DiscussionTopicContextType", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussionType", + "description": null, + "type": { + "kind": "ENUM", + "name": "DiscussionTopicDiscussionType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isAnnouncement", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isAnonymousAuthor", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ungradedDiscussionOverrides", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AssignmentOverrideCreateOrUpdate", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateDiscussionTopicPayload", + "description": "Autogenerated return type of CreateDiscussionTopic.", + "fields": [ + { + "name": "discussionTopic", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateGroupInSetInput", + "description": "Autogenerated input type of CreateGroupInSet", + "fields": null, + "inputFields": [ + { + "name": "groupSetId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nonCollaborative", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateGroupInSetPayload", + "description": "Autogenerated return type of CreateGroupInSet.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "group", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Group", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateGroupSetInput", + "description": "Autogenerated input type of CreateGroupSet", + "fields": null, + "inputFields": [ + { + "name": "contextId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "GroupSetContextType", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignAsync", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignUnassignedMembers", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "autoLeaderType", + "description": null, + "type": { + "kind": "ENUM", + "name": "AutoLeaderType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createGroupCount", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createGroupMemberCount", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enableAutoLeader", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enableSelfSignup", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupBySection", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupLimit", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nonCollaborative", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "restrictSelfSignup", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "selfSignup", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateGroupSetPayload", + "description": "Autogenerated return type of CreateGroupSet.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupSet", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "GroupSet", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateInternalSettingInput", + "description": "Autogenerated input type of CreateInternalSetting", + "fields": null, + "inputFields": [ + { + "name": "name", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "value", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateInternalSettingPayload", + "description": "Autogenerated return type of CreateInternalSetting.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "internalSetting", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "InternalSetting", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateLearningOutcomeGroupInput", + "description": "Autogenerated input type of CreateLearningOutcomeGroup", + "fields": null, + "inputFields": [ + { + "name": "description", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "vendorGuid", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateLearningOutcomeGroupPayload", + "description": "Autogenerated return type of CreateLearningOutcomeGroup.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "learningOutcomeGroup", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "LearningOutcomeGroup", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateLearningOutcomeInput", + "description": "Autogenerated input type of CreateLearningOutcome", + "fields": null, + "inputFields": [ + { + "name": "calculationInt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "calculationMethod", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "displayName", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "masteryPoints", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ratings", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ProficiencyRatingInput", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "vendorGuid", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateLearningOutcomePayload", + "description": "Autogenerated return type of CreateLearningOutcome.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "learningOutcome", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "LearningOutcome", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateModuleInput", + "description": "Autogenerated input type of CreateModule", + "fields": null, + "inputFields": [ + { + "name": "courseId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateModulePayload", + "description": "Autogenerated return type of CreateModule.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "module", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateOutcomeCalculationMethodInput", + "description": "Autogenerated input type of CreateOutcomeCalculationMethod", + "fields": null, + "inputFields": [ + { + "name": "calculationInt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "calculationMethod", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateOutcomeCalculationMethodPayload", + "description": "Autogenerated return type of CreateOutcomeCalculationMethod.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeCalculationMethod", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "OutcomeCalculationMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateOutcomeProficiencyInput", + "description": "Autogenerated input type of CreateOutcomeProficiency", + "fields": null, + "inputFields": [ + { + "name": "contextId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "proficiencyRatings", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "OutcomeProficiencyRatingCreate", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateOutcomeProficiencyPayload", + "description": "Autogenerated return type of CreateOutcomeProficiency.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeProficiency", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "OutcomeProficiency", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateSubmissionCommentInput", + "description": "Autogenerated input type of CreateSubmissionComment", + "fields": null, + "inputFields": [ + { + "name": "attempt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "comment", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "draftComment", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fileIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupComment", + "description": "Post comment to entire group (only relevant for group assignments grading students individually)", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaObjectId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaObjectType", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reviewerSubmissionId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateSubmissionCommentPayload", + "description": "Autogenerated return type of CreateSubmissionComment.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionComment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "SubmissionComment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateSubmissionDraftInput", + "description": "Autogenerated input type of CreateSubmissionDraft", + "fields": null, + "inputFields": [ + { + "name": "activeSubmissionType", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "DraftableSubmissionType", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attempt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "body", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "externalToolId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fileIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ltiLaunchUrl", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "resourceLinkLookupUuid", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateSubmissionDraftPayload", + "description": "Autogenerated return type of CreateSubmissionDraft.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionDraft", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "SubmissionDraft", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateSubmissionInput", + "description": "Autogenerated input type of CreateSubmission", + "fields": null, + "inputFields": [ + { + "name": "annotatableAttachmentId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "body", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fileIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "resourceLinkLookupUuid", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "studentId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionType", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "OnlineSubmissionType", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateSubmissionPayload", + "description": "Autogenerated return type of CreateSubmission.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submission", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Submission", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateUserInboxLabelInput", + "description": "Autogenerated input type of CreateUserInboxLabel", + "fields": null, + "inputFields": [ + { + "name": "names", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CreateUserInboxLabelPayload", + "description": "Autogenerated return type of CreateUserInboxLabel.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inboxLabels", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CustomGradeStatus", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "color", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CustomGradeStatusConnection", + "description": "The connection type for CustomGradeStatus.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CustomGradeStatusEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CustomGradeStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CustomGradeStatusEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "CustomGradeStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DashboardObserveeFilter", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "observedUserId", + "description": "Only view filtered user", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "SCALAR", + "name": "DateTime", + "description": "an ISO8601 formatted time string", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DateTimeRange", + "description": "a range of datetimes", + "fields": null, + "inputFields": [ + { + "name": "end", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "start", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteAccountDomainLookupInput", + "description": "Autogenerated input type of DeleteAccountDomainLookup", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DeleteAccountDomainLookupPayload", + "description": "Autogenerated return type of DeleteAccountDomainLookup.", + "fields": [ + { + "name": "accountDomainLookupId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteCommentBankItemInput", + "description": "Autogenerated input type of DeleteCommentBankItem", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DeleteCommentBankItemPayload", + "description": "Autogenerated return type of DeleteCommentBankItem.", + "fields": [ + { + "name": "commentBankItemId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteConversationMessagesInput", + "description": "Autogenerated input type of DeleteConversationMessages", + "fields": null, + "inputFields": [ + { + "name": "ids", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DeleteConversationMessagesPayload", + "description": "Autogenerated return type of DeleteConversationMessages.", + "fields": [ + { + "name": "conversationMessageIds", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteConversationsInput", + "description": "Autogenerated input type of DeleteConversations", + "fields": null, + "inputFields": [ + { + "name": "ids", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DeleteConversationsPayload", + "description": "Autogenerated return type of DeleteConversations.", + "fields": [ + { + "name": "conversationIds", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteCustomGradeStatusInput", + "description": "Autogenerated input type of DeleteCustomGradeStatus", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DeleteCustomGradeStatusPayload", + "description": "Autogenerated return type of DeleteCustomGradeStatus.", + "fields": [ + { + "name": "customGradeStatusId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteDiscussionEntryInput", + "description": "Autogenerated input type of DeleteDiscussionEntry", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DeleteDiscussionEntryPayload", + "description": "Autogenerated return type of DeleteDiscussionEntry.", + "fields": [ + { + "name": "discussionEntry", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntry", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteDiscussionTopicInput", + "description": "Autogenerated input type of DeleteDiscussionTopic", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DeleteDiscussionTopicPayload", + "description": "Autogenerated return type of DeleteDiscussionTopic.", + "fields": [ + { + "name": "discussionTopicId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteInternalSettingInput", + "description": "Autogenerated input type of DeleteInternalSetting", + "fields": null, + "inputFields": [ + { + "name": "internalSettingId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DeleteInternalSettingPayload", + "description": "Autogenerated return type of DeleteInternalSetting.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "internalSettingId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteOutcomeCalculationMethodInput", + "description": "Autogenerated input type of DeleteOutcomeCalculationMethod", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DeleteOutcomeCalculationMethodPayload", + "description": "Autogenerated return type of DeleteOutcomeCalculationMethod.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeCalculationMethodId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteOutcomeLinksInput", + "description": "Autogenerated input type of DeleteOutcomeLinks", + "fields": null, + "inputFields": [ + { + "name": "ids", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DeleteOutcomeLinksPayload", + "description": "Autogenerated return type of DeleteOutcomeLinks.", + "fields": [ + { + "name": "deletedOutcomeLinkIds", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteOutcomeProficiencyInput", + "description": "Autogenerated input type of DeleteOutcomeProficiency", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DeleteOutcomeProficiencyPayload", + "description": "Autogenerated return type of DeleteOutcomeProficiency.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeProficiencyId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteSubmissionCommentInput", + "description": "Autogenerated input type of DeleteSubmissionComment", + "fields": null, + "inputFields": [ + { + "name": "submissionCommentId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DeleteSubmissionCommentPayload", + "description": "Autogenerated return type of DeleteSubmissionComment.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionComment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "SubmissionComment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteSubmissionDraftInput", + "description": "Autogenerated input type of DeleteSubmissionDraft", + "fields": null, + "inputFields": [ + { + "name": "submissionId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DeleteSubmissionDraftPayload", + "description": "Autogenerated return type of DeleteSubmissionDraft.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionDraftIds", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteUserInboxLabelInput", + "description": "Autogenerated input type of DeleteUserInboxLabel", + "fields": null, + "inputFields": [ + { + "name": "names", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DeleteUserInboxLabelPayload", + "description": "Autogenerated return type of DeleteUserInboxLabel.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inboxLabels", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Discussion", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allowRating", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anonymousAuthor", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "AnonymousUser", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anonymousState", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "DiscussionTopicAnonymousStateType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attachment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "File", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "author", + "description": null, + "args": [ + { + "name": "builtInOnly", + "description": "Only return default/built_in roles", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "roleTypes", + "description": "Return only requested base role types", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "availableForUser", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canGroup", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canReplyAnonymously", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canUnpublish", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "childTopics", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextName", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseSections", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Section", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "delayedPostAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussionEntriesConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "ENUM", + "name": "DiscussionFilterType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rootEntries", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "searchTerm", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": null, + "type": { + "kind": "ENUM", + "name": "DiscussionSortOrderType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unreadBefore", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userSearchId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntryConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussionEntryDraftsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntryDraftConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussionType", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "DiscussionTopicDiscussionType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "editedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "editor", + "description": null, + "args": [ + { + "name": "builtInOnly", + "description": "Only return default/built_in roles", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "roleTypes", + "description": "Return only requested base role types", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "entriesTotalPages", + "description": null, + "args": [ + { + "name": "filter", + "description": null, + "type": { + "kind": "ENUM", + "name": "DiscussionFilterType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "perPage", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rootEntries", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "searchTerm", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": null, + "type": { + "kind": "ENUM", + "name": "DiscussionSortOrderType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unreadBefore", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "entryCounts", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntryCounts", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expanded", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expandedLocked", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupSet", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "GroupSet", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "initialPostRequiredForCurrentUser", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isAnnouncement", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isAnonymousAuthor", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isLockedByMasterCourse", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isSectionSpecific", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastReplyAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lockAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lockInformation", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locked", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mentionableUsersConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "searchTerm", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "MessageableUserConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "modules", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onlyGradersCanRate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onlyVisibleToOverrides", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "participant", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "DiscussionParticipant", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "permissions", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "DiscussionPermissions", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "podcastEnabled", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "podcastHasStudentPosts", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "position", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "published", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "replyToEntryRequiredCount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requireInitialPost", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rootEntriesTotalPages", + "description": null, + "args": [ + { + "name": "filter", + "description": null, + "type": { + "kind": "ENUM", + "name": "DiscussionFilterType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "perPage", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "searchTerm", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": null, + "type": { + "kind": "ENUM", + "name": "DiscussionSortOrderType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rootTopic", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "searchEntryCount", + "description": null, + "args": [ + { + "name": "filter", + "description": null, + "type": { + "kind": "ENUM", + "name": "DiscussionFilterType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "searchTerm", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortByRating", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": null, + "args": [ + { + "name": "sort", + "description": null, + "type": { + "kind": "ENUM", + "name": "DiscussionSortOrderType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "ENUM", + "name": "DiscussionSortOrderType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrderLocked", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscribed", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionDisabledForUser", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "todoDate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ISO8601DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ungradedDiscussionOverrides", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "AssignmentOverrideConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "visibleToEveryone", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "ModuleItemInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DiscussionCheckpointDate", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "dueAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lockAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "setId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "setType", + "description": null, + "type": { + "kind": "ENUM", + "name": "DiscussionCheckpointDateSetType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "studentIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "DiscussionCheckpointDateType", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unlockAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "DiscussionCheckpointDateSetType", + "description": "Types of date set that can be set for discussion checkpoints", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CourseSection", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Group", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ADHOC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Course", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "DiscussionCheckpointDateType", + "description": "Types of dates that can be set for discussion checkpoints", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "everyone", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "override", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DiscussionCheckpoints", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "checkpointLabel", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "CheckpointLabelType", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dates", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DiscussionCheckpointDate", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "repliesRequired", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DiscussionConnection", + "description": "The connection type for Discussion.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DiscussionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DiscussionEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DiscussionEntry", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allRootEntries", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DiscussionEntry", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anonymousAuthor", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "AnonymousUser", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attachment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "File", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "author", + "description": null, + "args": [ + { + "name": "builtInOnly", + "description": "Only return default/built_in roles", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "roleTypes", + "description": "Return only requested base role types", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleted", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "depth", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussionEntryVersions", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DiscussionEntryVersion", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussionSubentriesConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "beforeRelativeEntry", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "includeRelativeEntry", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "relativeEntryId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": null, + "type": { + "kind": "ENUM", + "name": "DiscussionSortOrderType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntryConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussionTopic", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussionTopicId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "editedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "editor", + "description": null, + "args": [ + { + "name": "builtInOnly", + "description": "Only return default/built_in roles", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "roleTypes", + "description": "Return only requested base role types", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "entryParticipant", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "EntryParticipant", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastReply", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntry", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "parentId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "permissions", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntryPermissions", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "previewMessage", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "quotedEntry", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntry", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ratingCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ratingSum", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reportTypeCounts", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntryReportTypeCounts", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rootEntry", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntry", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rootEntryId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rootEntryPageNumber", + "description": null, + "args": [ + { + "name": "perPage", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rootEntryParticipantCounts", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntryCounts", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subentriesCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DiscussionEntryConnection", + "description": "The connection type for DiscussionEntry.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DiscussionEntryEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DiscussionEntry", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DiscussionEntryCounts", + "description": null, + "fields": [ + { + "name": "deletedCount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "repliesCount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unreadCount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DiscussionEntryDraft", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attachment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "File", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussionEntryId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussionTopicId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "parentId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rootEntryId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DiscussionEntryDraftConnection", + "description": "The connection type for DiscussionEntryDraft.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DiscussionEntryDraftEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DiscussionEntryDraft", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DiscussionEntryDraftEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntryDraft", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DiscussionEntryEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntry", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DiscussionEntryPermissions", + "description": null, + "fields": [ + { + "name": "attach", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "create", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "delete", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "read", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reply", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "update", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "viewRating", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DiscussionEntryReportTypeCounts", + "description": null, + "fields": [ + { + "name": "inappropriateCount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "offensiveCount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "otherCount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "total", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DiscussionEntryVersion", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "version", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "DiscussionFilter", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "userId", + "description": "only return discussions for the given user. Defaults to\nthe current user.\n", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "searchTerm", + "description": "only return discussions whose title matches this search term\n", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "DiscussionFilterType", + "description": "Search types that can be associated with discussions", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "all", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unread", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "drafts", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleted", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DiscussionParticipant", + "description": null, + "fields": [ + { + "name": "expanded", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "DiscussionSortOrderType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "DiscussionPermissions", + "description": null, + "fields": [ + { + "name": "addRubric", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attach", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "closeForComments", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "copyAndSendTo", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "create", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "delete", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "duplicate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "manageAssignTo", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "manageCourseContentAdd", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "manageCourseContentDelete", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "manageCourseContentEdit", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "moderateForum", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "openForComments", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "peerReview", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "read", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "readAsAdmin", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "readReplies", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reply", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "showRubric", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "speedGrader", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "studentReporting", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "update", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "DiscussionSortOrderType", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "asc", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "desc", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "DiscussionTopicAnonymousStateType", + "description": "Anonymous states for discussionTopics", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "partial_anonymity", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "full_anonymity", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "off", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "DiscussionTopicContextType", + "description": "Context types that can be associated with discussionTopics", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "Course", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Group", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "DiscussionTopicDiscussionType", + "description": "Discussion type for discussionTopics", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "not_threaded", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "threaded", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "flat", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "side_comment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INTERFACE", + "name": "DiscussionsConnectionInterface", + "description": null, + "fields": [ + { + "name": "discussionsConnection", + "description": "returns a list of discussions.\n", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "DiscussionFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DiscussionConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Course", + "ofType": null + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "DraftableSubmissionType", + "description": "Types of submissions that can have a submission draft", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "basic_lti_launch", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "media_recording", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "online_text_entry", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "online_upload", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "online_url", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "student_annotation", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Enrollment", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assetString", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "associatedUser", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canBeRemoved", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "concluded", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "course", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Course", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseSectionId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "grades", + "description": null, + "args": [ + { + "name": "gradingPeriodId", + "description": "The grading period to return grades for. If not specified, will use the current grading period (or the course grade for courses that don't use grading periods)", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Grades", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "htmlUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastActivityAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "limitPrivilegesToCourseSection", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "section", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Section", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sisImportId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sisRole", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "EnrollmentWorkflowState", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalActivityTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "EnrollmentType", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "AssetString", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "EnrollmentConnection", + "description": "The connection type for Enrollment.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "EnrollmentEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Enrollment", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "EnrollmentEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Enrollment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "EnrollmentFilterInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "associatedUserIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": "[]", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "states", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "EnrollmentWorkflowState", + "ofType": null + } + } + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "types", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "EnrollmentType", + "ofType": null + } + } + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "EnrollmentType", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "StudentEnrollment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TeacherEnrollment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TaEnrollment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ObserverEnrollment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DesignerEnrollment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "StudentViewEnrollment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "EnrollmentWorkflowState", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "invited", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "creation_pending", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "active", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleted", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rejected", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "completed", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inactive", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "EntryParticipant", + "description": null, + "fields": [ + { + "name": "forcedReadState", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rating", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "read", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reportType", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ExternalTool", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canUnpublish", + "description": "Whether the module item can be unpublished", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isLockedByMasterCourse", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isLockedByMasterCourse", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "modules", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "published", + "description": "Whether the module item is published", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "settings", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "ExternalToolSettings", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "ExternalToolState", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "ModuleItemInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ExternalToolConnection", + "description": "The connection type for ExternalTool.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ExternalToolEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ExternalTool", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ExternalToolEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "ExternalTool", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "ExternalToolFilterInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "state", + "description": null, + "type": { + "kind": "ENUM", + "name": "ExternalToolState", + "ofType": null + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "placement", + "description": null, + "type": { + "kind": "ENUM", + "name": "ExternalToolPlacement", + "ofType": null + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "ExternalToolPlacement", + "description": "Placements that an External Tool can have", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "homework_submission", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ExternalToolPlacements", + "description": null, + "fields": [ + { + "name": "canvasIconClass", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "iconUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "messageType", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "text", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ExternalToolSettings", + "description": null, + "fields": [ + { + "name": "homeworkSubmission", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "ExternalToolPlacements", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "iconUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "selectionHeight", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "selectionWidth", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "text", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "ExternalToolState", + "description": "States that an External Tool can be in", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "anonymous", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name_only", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "email_only", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "public", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ExternalUrl", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canUnpublish", + "description": "Whether the module item can be unpublished", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isLockedByMasterCourse", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isLockedByMasterCourse", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "modules", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "newTab", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "published", + "description": "Whether the module item is published", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "ModuleItemInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "File", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canUnpublish", + "description": "Whether the module item can be unpublished", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contentType", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "displayName", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isLockedByMasterCourse", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isLockedByMasterCourse", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mimeClass", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "modules", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "published", + "description": "Whether the module item is published", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "size", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionPreviewUrl", + "description": null, + "args": [ + { + "name": "submissionId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "thumbnailUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "usageRights", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "UsageRights", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "wordCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "ModuleItemInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "FileConnection", + "description": "The connection type for File.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "FileEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "File", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "FileEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "File", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "FileFilter", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "userId", + "description": "only return files for the given user. Defaults to\nthe current user.\n", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "searchTerm", + "description": "only return files whose name matches this search term\n", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INTERFACE", + "name": "FilesConnectionInterface", + "description": null, + "fields": [ + { + "name": "filesConnection", + "description": "returns a list of files.\n", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "FileFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "FileConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Course", + "ofType": null + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "SCALAR", + "name": "Float", + "description": "Represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "GradeState", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "active", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleted", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Grades", + "description": "Contains grade information for a course or grading period", + "fields": [ + { + "name": "assignmentGroup", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "AssignmentGroup", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "currentGrade", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "currentScore", + "description": "The current score includes all graded assignments, excluding muted submissions.\n", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customGradeStatusId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enrollment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Enrollment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "finalGrade", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "finalScore", + "description": "The final score includes all assignments, excluding muted submissions\n(ungraded assignments are counted as 0 points).\n", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingPeriod", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "GradingPeriod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "overrideGrade", + "description": "The override grade. Supersedes the computed final grade if set.\n", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "overrideScore", + "description": "The override score. Supersedes the computed final score if set.\n", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "GradeState", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unpostedCurrentGrade", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unpostedCurrentScore", + "description": "The current score includes all graded assignments, including muted submissions.\n", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unpostedFinalGrade", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unpostedFinalScore", + "description": "The final score includes all assignments, including muted submissions\n(ungraded assignments are counted as 0 points).\n", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GradesConnection", + "description": "The connection type for Grades.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "GradesEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Grades", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GradesEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Grades", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "GradesEnrollmentFilter", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "enrollmentIds", + "description": "only include users with the given enrollment ids", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GradingPeriod", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "closeDate", + "description": "assignments can only be graded before the grading period closes\n", + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "displayTotals", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "endDate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isClosed", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isLast", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "startDate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "weight", + "description": "used to calculate how much the assignments in this grading period\ncontribute to the overall grade\n", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GradingPeriodConnection", + "description": "The connection type for GradingPeriod.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "GradingPeriodEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "GradingPeriod", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GradingPeriodEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "GradingPeriod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GradingPeriodGroup", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "displayTotals", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enrollmentTermIds", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingPeriodsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "GradingPeriodConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "weighted", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GradingStandard", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextCode", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "data", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "GradingStandardItem", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "migrationId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rootAccountId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "usageCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "version", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "workflowState", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GradingStandardItem", + "description": null, + "fields": [ + { + "name": "baseValue", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "letterGrade", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "GradingType", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "points", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "percent", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "letter_grade", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gpa_scale", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pass_fail", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "not_graded", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Group", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "activityStream", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "ActivityStream", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assetString", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canMessage", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "member", + "description": null, + "args": [ + { + "name": "userId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "GroupMembership", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "membersConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "GroupMembershipConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "membersCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nonCollaborative", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sisId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "AssetString", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GroupConnection", + "description": "The connection type for Group.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "GroupEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Group", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GroupEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Group", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GroupMembership", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "GroupMembershipState", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GroupMembershipConnection", + "description": "The connection type for GroupMembership.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "GroupMembershipEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "GroupMembership", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GroupMembershipEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "GroupMembership", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "GroupMembershipState", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "accepted", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "invited", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requested", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rejected", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleted", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GroupSet", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "autoLeader", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "AutoLeaderPolicy", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "currentGroup", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Group", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groups", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Group", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "GroupConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "memberLimit", + "description": "Sets a cap on the number of members in the group. Only applies when\nself-signup is enabled.\n", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nonCollaborative", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "selfSignup", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SelfSignupPolicy", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sisId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GroupSetConnection", + "description": "The connection type for GroupSet.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "GroupSetEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "GroupSet", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "GroupSetContextType", + "description": "Type of context for group set", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "account", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "course", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "GroupSetEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "GroupSet", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "HideAssignmentGradesForSectionsInput", + "description": "Autogenerated input type of HideAssignmentGradesForSections", + "fields": null, + "inputFields": [ + { + "name": "assignmentId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sectionIds", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "HideAssignmentGradesForSectionsPayload", + "description": "Autogenerated return type of HideAssignmentGradesForSections.", + "fields": [ + { + "name": "assignment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "progress", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Progress", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sections", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Section", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "HideAssignmentGradesInput", + "description": "Autogenerated input type of HideAssignmentGrades", + "fields": null, + "inputFields": [ + { + "name": "assignmentId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onlyStudentIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sectionIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "skipStudentIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "HideAssignmentGradesPayload", + "description": "Autogenerated return type of HideAssignmentGrades.", + "fields": [ + { + "name": "assignment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "progress", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Progress", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sections", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Section", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "Represents a unique identifier that is Base64 obfuscated. It is often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"VXNlci0xMA==\"`) or integer (such as `4`) input value will be accepted as an ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "SCALAR", + "name": "ISO8601DateTime", + "description": "An ISO 8601-encoded datetime", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": "https://tools.ietf.org/html/rfc3339", + "isOneOf": false + }, + { + "kind": "SCALAR", + "name": "ISO8601Duration", + "description": "An ISO 8601-encoded duration", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "ImportOutcomesInput", + "description": "Autogenerated input type of ImportOutcomes", + "fields": null, + "inputFields": [ + { + "name": "groupId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sourceContextId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sourceContextType", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "targetContextId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "targetContextType", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "targetGroupId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ImportOutcomesPayload", + "description": "Autogenerated return type of ImportOutcomes.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "progress", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Progress", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "InboxSettings", + "description": null, + "fields": [ + { + "name": "_id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outOfOfficeFirstDate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outOfOfficeLastDate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outOfOfficeMessage", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outOfOfficeSubject", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "signature", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "useOutOfOffice", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "useSignature", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "SCALAR", + "name": "Int", + "description": "Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "InternalSetting", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "secret", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "value", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "SCALAR", + "name": "JSON", + "description": "Represents untyped JSON", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "LatePolicyStatusType", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "late", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "missing", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "extended", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "none", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "LearningOutcome", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "alignments", + "description": null, + "args": [ + { + "name": "contextId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "OutcomeAlignment", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assessed", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "calculationInt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "calculationMethod", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canArchive", + "description": null, + "args": [ + { + "name": "contextId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canEdit", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "displayName", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "friendlyDescription", + "description": null, + "args": [ + { + "name": "contextId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "OutcomeFriendlyDescriptionType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isImported", + "description": null, + "args": [ + { + "name": "targetContextId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "targetContextType", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "masteryPoints", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ratings", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ProficiencyRating", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "vendorGuid", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "LearningOutcomeGroup", + "description": "Learning Outcome Group", + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canEdit", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "childGroups", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "LearningOutcomeGroupConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "childGroupsCount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notImportedOutcomesCount", + "description": null, + "args": [ + { + "name": "targetGroupId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomes", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "searchQuery", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ContentTagConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomesCount", + "description": null, + "args": [ + { + "name": "searchQuery", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "parentOutcomeGroup", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "LearningOutcomeGroup", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "vendorGuid", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "LearningOutcomeGroupConnection", + "description": "The connection type for LearningOutcomeGroup.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "LearningOutcomeGroupEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "LearningOutcomeGroup", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "LearningOutcomeGroupEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "LearningOutcomeGroup", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Account", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "AccountDomain", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "AccountDomainLookup", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "AssessmentRequest", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "AssignmentGroup", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "CommentBankItem", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "CommunicationChannel", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ContentTag", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Course", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "CustomGradeStatus", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "DiscussionEntry", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "DiscussionEntryDraft", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "DiscussionEntryVersion", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ExternalTool", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ExternalUrl", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "File", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "GradingPeriod", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "GradingPeriodGroup", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Group", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "GroupMembership", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "GroupSet", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "InternalSetting", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "LearningOutcome", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "LearningOutcomeGroup", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "MediaTrack", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Module", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ModuleExternalTool", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ModuleItem", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Notification", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "NotificationPolicy", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "OutcomeCalculationMethod", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "OutcomeFriendlyDescriptionType", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "OutcomeProficiency", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Page", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "PostPolicy", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ProficiencyRating", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Progress", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Quiz", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Rubric", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RubricAssessment", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RubricAssociation", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RubricCriterion", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RubricRating", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Section", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "StandardGradeStatus", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Submission", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SubmissionComment", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SubmissionDraft", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Term", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "UsageRights", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "LockInfo", + "description": null, + "fields": [ + { + "name": "canView", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isLocked", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lockAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lockedObject", + "description": null, + "args": [], + "type": { + "kind": "UNION", + "name": "Lockable", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "module", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unlockAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "UNION", + "name": "Lockable", + "description": "Types that can be locked", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Module", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Page", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Quiz", + "ofType": null + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "MarkSubmissionCommentsReadInput", + "description": "Autogenerated input type of MarkSubmissionCommentsRead", + "fields": null, + "inputFields": [ + { + "name": "submissionCommentIds", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "MarkSubmissionCommentsReadPayload", + "description": "Autogenerated return type of MarkSubmissionCommentsRead.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionComments", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SubmissionComment", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "MediaObject", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canAddCaptions", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaDownloadUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaSources", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MediaSource", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaTracks", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MediaTrack", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaType", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "MediaType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "thumbnailUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "MediaSource", + "description": null, + "fields": [ + { + "name": "bitrate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contentType", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fileExt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "height", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isOriginal", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "size", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "width", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "MediaTrack", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "content", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locale", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaObject", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "MediaObject", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "webvttContent", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "MediaType", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "audio", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "video", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "MessagePermissions", + "description": null, + "fields": [ + { + "name": "sendMessages", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sendMessagesAll", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "MessageableContext", + "description": null, + "fields": [ + { + "name": "avatarUrl", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "itemCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "permissions", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "MessagePermissions", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "MessageableContextConnection", + "description": "The connection type for MessageableContext.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MessageableContextEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MessageableContext", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "MessageableContextEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MessageableContext", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "MessageableUser", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "commonCoursesConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "EnrollmentConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "commonGroupsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "GroupConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "observerEnrollmentsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextCode", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "EnrollmentConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pronouns", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "shortName", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "MessageableUserConnection", + "description": "The connection type for MessageableUser.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MessageableUserEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MessageableUser", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "MessageableUserEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MessageableUser", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ModeratedGrading", + "description": "Settings for Moderated Grading on an Assignment", + "fields": [ + { + "name": "enabled", + "description": "Boolean indicating if the assignment is moderated.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "finalGrader", + "description": "The user of the grader responsible for choosing final grades for this assignment.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "graderCommentsVisibleToGraders", + "description": "Boolean indicating if provisional graders' comments are visible to other provisional graders.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "graderCount", + "description": "The maximum number of provisional graders who may issue grades for this assignment.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "graderNamesVisibleToFinalGrader", + "description": "Boolean indicating if provisional graders' identities are hidden from other provisional graders.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradersAnonymousToGraders", + "description": "Boolean indicating if provisional grader identities are visible to the final grader.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Module", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "completionRequirements", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ModuleCompletionRequirement", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "estimatedDuration", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ISO8601Duration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "estimatedDuration", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ISO8601Duration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "moduleItems", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ModuleItem", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "position", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "prerequisites", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ModulePrerequisite", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "published", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requirementCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unlockAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ModuleCompletionRequirement", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "minPercentage", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "minScore", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ModuleConnection", + "description": "The connection type for Module.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ModuleEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ModuleEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ModuleExternalTool", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canUnpublish", + "description": "Whether the module item can be unpublished", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isLockedByMasterCourse", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "modules", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "published", + "description": "Whether the module item is published", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "ModuleItemInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ModuleItem", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "content", + "description": null, + "args": [], + "type": { + "kind": "INTERFACE", + "name": "ModuleItemInterface", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "estimatedDuration", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ISO8601Duration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "estimatedDuration", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ISO8601Duration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "indent", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "indent", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "module", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "next", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "ModuleItem", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nextItemsConnection", + "description": "Items are ordered based on distance to the current item, starting with the next item directly following it.", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ModuleItemConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "previous", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "ModuleItem", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "previousItemsConnection", + "description": "Items are ordered based on distance to the current item, starting with the previous item directly preceding it.", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ModuleItemConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ModuleItemConnection", + "description": "The connection type for ModuleItem.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ModuleItemEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ModuleItem", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ModuleItemEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "ModuleItem", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INTERFACE", + "name": "ModuleItemInterface", + "description": "An item that can be in context modules", + "fields": [ + { + "name": "canUnpublish", + "description": "Whether the module item can be unpublished", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isLockedByMasterCourse", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "modules", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "published", + "description": "Whether the module item is published", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ExternalTool", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ExternalUrl", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "File", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ModuleExternalTool", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Page", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Quiz", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SubHeader", + "ofType": null + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ModulePrerequisite", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ModuleProgression", + "description": null, + "fields": [ + { + "name": "incompleteItemsConnection", + "description": "Items are ordered by position", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ModuleItemConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "module", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ModuleProgressionConnection", + "description": "The connection type for ModuleProgression.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ModuleProgressionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ModuleProgression", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ModuleProgressionEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "ModuleProgression", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "MoveOutcomeLinksInput", + "description": "Autogenerated input type of MoveOutcomeLinks", + "fields": null, + "inputFields": [ + { + "name": "outcomeLinkIds", + "description": "A list of ContentTags that will be moved\n", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupId", + "description": "The id of the destination group\n", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "MoveOutcomeLinksPayload", + "description": "Autogenerated return type of MoveOutcomeLinks.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "movedOutcomeLinks", + "description": "List of Outcome Links that were sucessfully moved to the group\n", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ContentTag", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Mutation", + "description": null, + "fields": [ + { + "name": "addConversationMessage", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for AddConversationMessage", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AddConversationMessageInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "AddConversationMessagePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createAccountDomainLookup", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateAccountDomainLookup", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateAccountDomainLookupInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateAccountDomainLookupPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createAssignment", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateAssignment", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateAssignmentInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateAssignmentPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createCommentBankItem", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateCommentBankItem", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateCommentBankItemInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateCommentBankItemPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createConversation", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateConversation", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateConversationInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateConversationPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createDiscussionEntry", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateDiscussionEntry", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateDiscussionEntryInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateDiscussionEntryPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createDiscussionEntryDraft", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateDiscussionEntryDraft", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateDiscussionEntryDraftInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateDiscussionEntryDraftPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createDiscussionTopic", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateDiscussionTopic", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateDiscussionTopicInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateDiscussionTopicPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createGroupInSet", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateGroupInSet", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateGroupInSetInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateGroupInSetPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createGroupSet", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateGroupSet", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateGroupSetInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateGroupSetPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createInternalSetting", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateInternalSetting", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateInternalSettingInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateInternalSettingPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createLearningOutcome", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateLearningOutcome", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateLearningOutcomeInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateLearningOutcomePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createLearningOutcomeGroup", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateLearningOutcomeGroup", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateLearningOutcomeGroupInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateLearningOutcomeGroupPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createModule", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateModule", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateModuleInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateModulePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createOutcomeCalculationMethod", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateOutcomeCalculationMethod", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateOutcomeCalculationMethodInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateOutcomeCalculationMethodPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createOutcomeProficiency", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateOutcomeProficiency", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateOutcomeProficiencyInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateOutcomeProficiencyPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createSubmission", + "description": "IN ACTIVE DEVELOPMENT, USE AT YOUR OWN RISK: Submit homework on an assignment.\n", + "args": [ + { + "name": "input", + "description": "Parameters for CreateSubmission", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateSubmissionInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateSubmissionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createSubmissionComment", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateSubmissionComment", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateSubmissionCommentInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateSubmissionCommentPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createSubmissionDraft", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateSubmissionDraft", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateSubmissionDraftInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateSubmissionDraftPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createUserInboxLabel", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for CreateUserInboxLabel", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateUserInboxLabelInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateUserInboxLabelPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteAccountDomainLookup", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DeleteAccountDomainLookup", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteAccountDomainLookupInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteAccountDomainLookupPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteCommentBankItem", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DeleteCommentBankItem", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteCommentBankItemInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteCommentBankItemPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteConversationMessages", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DeleteConversationMessages", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteConversationMessagesInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteConversationMessagesPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteConversations", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DeleteConversations", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteConversationsInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteConversationsPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteCustomGradeStatus", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DeleteCustomGradeStatus", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteCustomGradeStatusInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteCustomGradeStatusPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteDiscussionEntry", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DeleteDiscussionEntry", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteDiscussionEntryInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteDiscussionEntryPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteDiscussionTopic", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DeleteDiscussionTopic", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteDiscussionTopicInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteDiscussionTopicPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteInternalSetting", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DeleteInternalSetting", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteInternalSettingInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteInternalSettingPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteOutcomeCalculationMethod", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DeleteOutcomeCalculationMethod", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteOutcomeCalculationMethodInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteOutcomeCalculationMethodPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteOutcomeLinks", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DeleteOutcomeLinks", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteOutcomeLinksInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteOutcomeLinksPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteOutcomeProficiency", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DeleteOutcomeProficiency", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteOutcomeProficiencyInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteOutcomeProficiencyPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteSubmissionComment", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DeleteSubmissionComment", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteSubmissionCommentInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteSubmissionCommentPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteSubmissionDraft", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DeleteSubmissionDraft", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteSubmissionDraftInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteSubmissionDraftPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteUserInboxLabel", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for DeleteUserInboxLabel", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteUserInboxLabelInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteUserInboxLabelPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hideAssignmentGrades", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for HideAssignmentGrades", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "HideAssignmentGradesInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "HideAssignmentGradesPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hideAssignmentGradesForSections", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for HideAssignmentGradesForSections", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "HideAssignmentGradesForSectionsInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "HideAssignmentGradesForSectionsPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "importOutcomes", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for ImportOutcomes", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ImportOutcomesInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ImportOutcomesPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "markSubmissionCommentsRead", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for MarkSubmissionCommentsRead", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "MarkSubmissionCommentsReadInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "MarkSubmissionCommentsReadPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "moveOutcomeLinks", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for MoveOutcomeLinks", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "MoveOutcomeLinksInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "MoveOutcomeLinksPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postAssignmentGrades", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for PostAssignmentGrades", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PostAssignmentGradesInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PostAssignmentGradesPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postAssignmentGradesForSections", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for PostAssignmentGradesForSections", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PostAssignmentGradesForSectionsInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PostAssignmentGradesForSectionsPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postDraftSubmissionComment", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for PostDraftSubmissionComment", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PostDraftSubmissionCommentInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PostDraftSubmissionCommentPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "setAssignmentPostPolicy", + "description": "Sets the post policy for the assignment.\n", + "args": [ + { + "name": "input", + "description": "Parameters for SetAssignmentPostPolicy", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SetAssignmentPostPolicyInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SetAssignmentPostPolicyPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "setCoursePostPolicy", + "description": "Sets the post policy for the course, with an option to override and delete\nexisting assignment post policies.\n", + "args": [ + { + "name": "input", + "description": "Parameters for SetCoursePostPolicy", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SetCoursePostPolicyInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SetCoursePostPolicyPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "setFriendlyDescription", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for SetFriendlyDescription", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SetFriendlyDescriptionInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SetFriendlyDescriptionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "setModuleItemCompletion", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for SetModuleItemCompletion", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SetModuleItemCompletionInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SetModuleItemCompletionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "setOverrideScore", + "description": "Sets the overridden final score for the associated enrollment, optionally limited to a specific\ngrading period. This will supersede the computed final score/grade if present.\n", + "args": [ + { + "name": "input", + "description": "Parameters for SetOverrideScore", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SetOverrideScoreInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SetOverrideScorePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "setOverrideStatus", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for SetOverrideStatus", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SetOverrideStatusInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SetOverrideStatusPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "setRubricSelfAssessment", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for SetRubricSelfAssessment", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SetRubricSelfAssessmentInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SetRubricSelfAssessmentPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscribeToDiscussionTopic", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for SubscribeToDiscussionTopic", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SubscribeToDiscussionTopicInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SubscribeToDiscussionTopicPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateAccountDomainLookup", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateAccountDomainLookup", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateAccountDomainLookupInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateAccountDomainLookupPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateAssignment", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateAssignment", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateAssignmentInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateAssignmentPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateCommentBankItem", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateCommentBankItem", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateCommentBankItemInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateCommentBankItemPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateConversationParticipants", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateConversationParticipants", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateConversationParticipantsInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateConversationParticipantsPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateDiscussionEntriesReadState", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateDiscussionEntriesReadState", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionEntriesReadStateInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateDiscussionEntriesReadStatePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateDiscussionEntry", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateDiscussionEntry", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionEntryInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateDiscussionEntryPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateDiscussionEntryParticipant", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateDiscussionEntryParticipant", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionEntryParticipantInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateDiscussionEntryParticipantPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateDiscussionExpanded", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateDiscussionExpanded", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionExpandedInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateDiscussionExpandedPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateDiscussionReadState", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateDiscussionReadState", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionReadStateInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateDiscussionReadStatePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateDiscussionSortOrder", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateDiscussionSortOrder", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionSortOrderInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateDiscussionSortOrderPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateDiscussionThreadReadState", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateDiscussionThreadReadState", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionThreadReadStateInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateDiscussionThreadReadStatePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateDiscussionTopic", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateDiscussionTopic", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionTopicInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateDiscussionTopicPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateGradebookGroupFilter", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateGradebookGroupFilter", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateGradebookGroupFilterInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateGradebookGroupFilterPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateGradebookGroupFilter", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateGradebookGroupFilter", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateGradebookGroupFilterInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateGradebookGroupFilterPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateInternalSetting", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateInternalSetting", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateInternalSettingInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateInternalSettingPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateLearningOutcome", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateLearningOutcome", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateLearningOutcomeInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateLearningOutcomePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateLearningOutcomeGroup", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateLearningOutcomeGroup", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateLearningOutcomeGroupInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateLearningOutcomeGroupPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateMyInboxSettings", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateMyInboxSettings", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateMyInboxSettingsInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateMyInboxSettingsPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateNotificationPreferences", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateNotificationPreferences", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateNotificationPreferencesInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateNotificationPreferencesPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateOutcomeCalculationMethod", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateOutcomeCalculationMethod", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateOutcomeCalculationMethodInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateOutcomeCalculationMethodPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateOutcomeProficiency", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateOutcomeProficiency", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateOutcomeProficiencyInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateOutcomeProficiencyPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateRubricArchivedState", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateRubricArchivedState", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateRubricArchivedStateInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateRubricArchivedStatePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateRubricAssessmentReadState", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateRubricAssessmentReadState", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateRubricAssessmentReadStateInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateRubricAssessmentReadStatePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateSpeedGraderSettings", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateSpeedGraderSettings", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateSpeedGraderSettingsInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateSpeedGraderSettingsPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateSplitScreenViewDeeplyNestedAlert", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateSplitScreenViewDeeplyNestedAlert", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateSplitScreenViewDeeplyNestedAlertInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateSplitScreenViewDeeplyNestedAlertPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateSubmissionGrade", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateSubmissionsGrade", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateSubmissionsGradeInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateSubmissionsGradePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateSubmissionGradeStatus", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateSubmissionsGradeStatus", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateSubmissionsGradeStatusInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateSubmissionsGradeStatusPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateSubmissionSticker", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateSubmissionSticker", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateSubmissionStickerInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateSubmissionStickerPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateSubmissionStudentEnteredScore", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateSubmissionStudentEnteredScore", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateSubmissionStudentEnteredScoreInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateSubmissionStudentEnteredScorePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateSubmissionsReadState", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateSubmissionsReadState", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateSubmissionsReadStateInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateSubmissionsReadStatePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateUserDiscussionsSplitscreenView", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpdateUserDiscussionsSplitscreenView", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateUserDiscussionsSplitscreenViewInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateUserDiscussionsSplitscreenViewPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "upsertCustomGradeStatus", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpsertCustomGradeStatus", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpsertCustomGradeStatusInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpsertCustomGradeStatusPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "upsertStandardGradeStatus", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for UpsertStandardGradeStatus", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpsertStandardGradeStatusInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpsertStandardGradeStatusPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "MutationLog", + "description": null, + "fields": [ + { + "name": "assetString", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationName", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "params", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "JSON", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "realUser", + "description": "If the mutation was performed by a user masquerading as another user,\nthis field returns the \"real\" (logged-in) user.\n", + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "MutationLogConnection", + "description": "The connection type for MutationLog.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MutationLogEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MutationLog", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "MutationLogEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MutationLog", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INTERFACE", + "name": "Node", + "description": "An object with an ID.", + "fields": [ + { + "name": "id", + "description": "ID of the object.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Account", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "AssignmentGroup", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "CommentBankItem", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "CommunicationChannel", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ContentTag", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Conversation", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Course", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "CustomGradeStatus", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "DiscussionEntry", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Enrollment", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "File", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "GradingPeriod", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "GradingPeriodGroup", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "GradingStandard", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Group", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "GroupSet", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "InternalSetting", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "LearningOutcome", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "LearningOutcomeGroup", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "MediaObject", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "MessageableContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "MessageableUser", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Module", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ModuleItem", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Notification", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "NotificationPolicy", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "OutcomeCalculationMethod", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "OutcomeFriendlyDescriptionType", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "OutcomeProficiency", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Page", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "PostPolicy", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Progress", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Quiz", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Rubric", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Section", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "StandardGradeStatus", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Submission", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Term", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "UsageRights", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "NodeType", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "Account", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Assignment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AssignmentGroup", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Conversation", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Course", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Discussion", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DiscussionEntry", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Enrollment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "File", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GradingPeriod", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GradingPeriodGroup", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Group", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GroupSet", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "InternalSetting", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LearningOutcomeGroup", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MediaObject", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Module", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ModuleItem", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OutcomeCalculationMethod", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OutcomeProficiency", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Page", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PostPolicy", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Progress", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Rubric", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Section", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Submission", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Term", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UsageRights", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "User", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Noop", + "description": "A descriptive tag that doesn't link the assignment to a set", + "fields": [ + { + "name": "_id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Notification", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "category", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "categoryDescription", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "categoryDisplayName", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "workflowState", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "NotificationCategoryType", + "description": "The categories that a notification can belong to", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "Account_Notification", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Added_To_Conversation", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "All_Submissions", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Announcement", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Announcement_Created_By_You", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Appointment_Availability", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Appointment_Cancelations", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Appointment_Signups", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Blueprint", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Calendar", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Content_Link_Error", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Conversation_Created", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Conversation_Message", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Course_Content", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Discussion", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DiscussionEntry", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DiscussionMention", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ReportedReply", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Due_Date", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Files", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Grading", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Grading_Policies", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Invitation", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Late_Grading", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Membership_Update", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Other", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Recording_Ready", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Student_Appointment_Signups", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Submission_Comment", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "NotificationFrequencyType", + "description": "Frequency that notifications can be delivered on", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "immediately", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "daily", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "weekly", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "never", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "NotificationPolicy", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "communicationChannelId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "frequency", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notification", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Notification", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "NotificationPreferences", + "description": null, + "fields": [ + { + "name": "channels", + "description": null, + "args": [ + { + "name": "channelId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CommunicationChannel", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "readPrivacyNoticeDate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sendObservedNamesInNotifications", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sendScoresInEmails", + "description": null, + "args": [ + { + "name": "courseId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "NotificationPreferencesContextType", + "description": "Context types that can be associated with notification preferences", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "Course", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Account", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "OnlineSubmissionType", + "description": "Types that can be submitted online", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "basic_lti_launch", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "student_annotation", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "media_recording", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "online_text_entry", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "online_upload", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "online_url", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "OrderDirection", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ascending", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "descending", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "OutcomeAlignment", + "description": null, + "fields": [ + { + "name": "_id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "alignmentsCount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentContentType", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentWorkflowState", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contentId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contentType", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "learningOutcomeId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "moduleId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "moduleName", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "moduleUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "moduleWorkflowState", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "quizItems", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "QuizItem", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "OutcomeCalculationMethod", + "description": "Customized calculation method", + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "calculationInt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "calculationMethod", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locked", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "OutcomeFriendlyDescriptionType", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "learningOutcomeId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "workflowState", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "OutcomeProficiency", + "description": "Customized proficiency ratings", + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locked", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "proficiencyRatingsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ProficiencyRatingConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "OutcomeProficiencyRatingCreate", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "color", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mastery", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "points", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Page", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canUnpublish", + "description": "Whether the module item can be unpublished", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isLockedByMasterCourse", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "modules", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "published", + "description": "Whether the module item is published", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "ModuleItemInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "PageConnection", + "description": "The connection type for Page.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Page", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "PageEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Page", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "PageFilter", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "userId", + "description": "only return pages for the given user. Defaults to\nthe current user.\n", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "searchTerm", + "description": "only return pages whose title matches this search term\n", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "PageInfo", + "description": "Information about pagination in a connection.", + "fields": [ + { + "name": "endCursor", + "description": "When paginating forwards, the cursor to continue.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasNextPage", + "description": "When paginating forwards, are there more items?", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasPreviousPage", + "description": "When paginating backwards, are there more items?", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "startCursor", + "description": "When paginating backwards, the cursor to continue.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "PageViewAnalysis", + "description": null, + "fields": [ + { + "name": "level", + "description": "This number (0-3) is intended to give an idea of how the student is doing relative to others in the course", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "max", + "description": "The maximum number of views/participations in this course", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "total", + "description": "The number of views/participations this student has", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INTERFACE", + "name": "PagesConnectionInterface", + "description": null, + "fields": [ + { + "name": "pagesConnection", + "description": "returns a list of wiki pages.\n", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "PageFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PageConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Course", + "ofType": null + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "PeerReviews", + "description": "Settings for Peer Reviews on an Assignment", + "fields": [ + { + "name": "anonymousReviews", + "description": "Boolean representing whether or not peer reviews are anonymous", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "automaticReviews", + "description": "Boolean indicating peer reviews are assigned automatically. If false, the teacher is expected to manually assign peer reviews.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "count", + "description": "Integer representing the amount of reviews each user is assigned.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dueAt", + "description": "Date and Time representing when the peer reviews are due", + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enabled", + "description": "Boolean indicating if peer reviews are required for this assignment", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "intraReviews", + "description": "Boolean representing whether or not members from within the same group on a group assignment can be assigned to peer review their own group's work", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "PostAssignmentGradesForSectionsInput", + "description": "Autogenerated input type of PostAssignmentGradesForSections", + "fields": null, + "inputFields": [ + { + "name": "assignmentId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradedOnly", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sectionIds", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "PostAssignmentGradesForSectionsPayload", + "description": "Autogenerated return type of PostAssignmentGradesForSections.", + "fields": [ + { + "name": "assignment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "progress", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Progress", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sections", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Section", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "PostAssignmentGradesInput", + "description": "Autogenerated input type of PostAssignmentGrades", + "fields": null, + "inputFields": [ + { + "name": "assignmentId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradedOnly", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onlyStudentIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sectionIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "skipStudentIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "PostAssignmentGradesPayload", + "description": "Autogenerated return type of PostAssignmentGrades.", + "fields": [ + { + "name": "assignment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "progress", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Progress", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sections", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Section", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "PostDraftSubmissionCommentInput", + "description": "Autogenerated input type of PostDraftSubmissionComment", + "fields": null, + "inputFields": [ + { + "name": "submissionCommentId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "PostDraftSubmissionCommentPayload", + "description": "Autogenerated return type of PostDraftSubmissionComment.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionComment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "SubmissionComment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "PostPolicy", + "description": "A PostPolicy sets the policy for whether a Submission's grades are posted\nautomatically or manually. A PostPolicy can be set at the Course and/or\nAssignment level.\n", + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "course", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Course", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "ID of the object.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postManually", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "PostPolicyConnection", + "description": "The connection type for PostPolicy.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PostPolicyEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PostPolicy", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "PostPolicyEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PostPolicy", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ProficiencyRating", + "description": "Customized proficiency rating", + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "color", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mastery", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "points", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ProficiencyRatingConnection", + "description": "The connection type for ProficiencyRating.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ProficiencyRatingEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ProficiencyRating", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ProficiencyRatingEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "ProficiencyRating", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "ProficiencyRatingInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "description", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "points", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Progress", + "description": "Returns completion status and progress information about an asynchronous job", + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "completion", + "description": "percent completed", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "context", + "description": null, + "args": [], + "type": { + "kind": "UNION", + "name": "ProgressContext", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": "details about the job", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "ProgressState", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tag", + "description": "the type of operation", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "UNION", + "name": "ProgressContext", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Course", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "File", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "GroupSet", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "ProgressState", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "queued", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "running", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "completed", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "failed", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Query", + "description": null, + "fields": [ + { + "name": "account", + "description": null, + "args": [ + { + "name": "id", + "description": "a graphql or legacy id", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sisId", + "description": "a id from the original SIS system", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Account", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allCourses", + "description": "All courses viewable by the current user", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Course", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignment", + "description": null, + "args": [ + { + "name": "id", + "description": "a graphql or legacy id", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sisId", + "description": "an id from the original SIS system", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentGroup", + "description": null, + "args": [ + { + "name": "id", + "description": "a graphql or legacy id", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sisId", + "description": "an id from the original SIS system", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "AssignmentGroup", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "auditLogs", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "AuditLogs", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "course", + "description": null, + "args": [ + { + "name": "id", + "description": "a graphql or legacy id, preference for search is given to this id", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sisId", + "description": "a id from the original SIS system", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Course", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "internalSetting", + "description": "Retrieves a single internal setting by its ID or name", + "args": [ + { + "name": "id", + "description": "a graphql or legacy id", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "the name of the Setting", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "InternalSetting", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "internalSettings", + "description": "All internal settings", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "InternalSetting", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "learningOutcome", + "description": "LearningOutcome", + "args": [ + { + "name": "id", + "description": "a graphql or legacy id", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "LearningOutcome", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "learningOutcomeGroup", + "description": "LearningOutcomeGroup", + "args": [ + { + "name": "id", + "description": "a graphql or legacy id", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "LearningOutcomeGroup", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "legacyNode", + "description": "Fetches an object given its type and legacy ID", + "args": [ + { + "name": "_id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "NodeType", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "moduleItem", + "description": "ModuleItem", + "args": [ + { + "name": "id", + "description": "a graphql or legacy id", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ModuleItem", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "myInboxSettings", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "InboxSettings", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "Fetches an object given its ID.", + "args": [ + { + "name": "id", + "description": "ID of the object.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeCalculationMethod", + "description": "OutcomeCalculationMethod", + "args": [ + { + "name": "id", + "description": "a graphql or legacy id", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "OutcomeCalculationMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeProficiency", + "description": "OutcomeProficiency", + "args": [ + { + "name": "id", + "description": "a graphql or legacy id", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "OutcomeProficiency", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubric", + "description": "Rubric", + "args": [ + { + "name": "id", + "description": "a graphql or legacy id", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Rubric", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submission", + "description": null, + "args": [ + { + "name": "id", + "description": "a graphql or legacy id", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentId", + "description": "a graphql or legacy assignment id", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userId", + "description": "a graphql or legacy user id", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anonymousId", + "description": "an anonymous id in use when grading anonymously", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Submission", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "term", + "description": null, + "args": [ + { + "name": "id", + "description": "a graphql or legacy id", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sisId", + "description": "an id from the original SIS system", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Term", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Quiz", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anonymousSubmissions", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canUnpublish", + "description": "Whether the module item can be unpublished", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isLockedByMasterCourse", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "modules", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "published", + "description": "Whether the module item is published", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "ModuleItemInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "QuizConnection", + "description": "The connection type for Quiz.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "QuizEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Quiz", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "QuizEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Quiz", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "QuizFilter", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "userId", + "description": "only return quizzes for the given user. Defaults to\nthe current user.\n", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "searchTerm", + "description": "only return quizzes whose title matches this search term\n", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "QuizItem", + "description": null, + "fields": [ + { + "name": "_id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INTERFACE", + "name": "QuizzesConnectionInterface", + "description": null, + "fields": [ + { + "name": "quizzesConnection", + "description": "returns a list of quizzes.\n", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "QuizFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "QuizConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Course", + "ofType": null + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "RatingInputType", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "not_liked", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "liked", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Recipients", + "description": null, + "fields": [ + { + "name": "contextsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "MessageableContextConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sendMessagesAll", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "usersConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "MessageableUserConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "ReportType", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "inappropriate", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "offensive", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "other", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Rubric", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "buttonDisplay", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "criteria", + "description": "The different criteria that makes up this rubric\n", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "RubricCriterion", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "criteriaCount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "freeFormCriterionComments", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasRubricAssociations", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hidePoints", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hideScoreTotal", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ratingOrder", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubricAssociationForContext", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "RubricAssociation", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unassessed", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "workflowState", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "RubricAssessment", + "description": "An assessment for a rubric", + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "artifactAttempt", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assessmentRatings", + "description": "The assessments for the individual criteria in this rubric\n", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "RubricAssessmentRating", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assessmentType", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "AssessmentType", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assessor", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubricAssociation", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "RubricAssociation", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "score", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "RubricAssessmentConnection", + "description": "The connection type for RubricAssessment.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "RubricAssessmentEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "RubricAssessment", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "RubricAssessmentEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "RubricAssessment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "RubricAssessmentRating", + "description": "An assessment for a specific criteria in a rubric", + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "artifactAttempt", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "comments", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "commentsEnabled", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "commentsHtml", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "criterion", + "description": "The rubric criteria that this assessment is for\n", + "args": [], + "type": { + "kind": "OBJECT", + "name": "RubricCriterion", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcome", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "LearningOutcome", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "points", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubricAssessmentId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "RubricAssociation", + "description": "How a rubric is being used in a context", + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hideOutcomeResults", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hidePoints", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hideScoreTotal", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "savedComments", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "useForGrading", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "RubricConnection", + "description": "The connection type for Rubric.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "RubricEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Rubric", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "RubricCriterion", + "description": "Individual criteria for a rubric", + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "criterionUseRange", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ignoreForScoring", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "learningOutcomeId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "longDescription", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "masteryPoints", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcome", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "LearningOutcome", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "points", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ratings", + "description": "The possible ratings available for this criterion\n", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "RubricRating", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "RubricEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Rubric", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "RubricRating", + "description": "Possible rating for a rubric criterion", + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "longDescription", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "points", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubricId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Section", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allStudents", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UserConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradesPosted", + "description": null, + "args": [ + { + "name": "assignmentId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sisId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "students", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UserConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userCount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SectionConnection", + "description": "The connection type for Section.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SectionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Section", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SectionEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Section", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "SelfSignupPolicy", + "description": "Determines if/how a student may join a group. A student can belong to\nonly one group per group set at a time.\n", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "enabled", + "description": "students may join any group", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "restricted", + "description": "students may join a group in their section", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "disabled", + "description": "self signup is not allowed", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SetAssignmentPostPolicyInput", + "description": "Autogenerated input type of SetAssignmentPostPolicy", + "fields": null, + "inputFields": [ + { + "name": "assignmentId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postManually", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SetAssignmentPostPolicyPayload", + "description": "Autogenerated return type of SetAssignmentPostPolicy.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postPolicy", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "PostPolicy", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SetCoursePostPolicyInput", + "description": "Autogenerated input type of SetCoursePostPolicy", + "fields": null, + "inputFields": [ + { + "name": "courseId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postManually", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SetCoursePostPolicyPayload", + "description": "Autogenerated return type of SetCoursePostPolicy.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postPolicy", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "PostPolicy", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SetFriendlyDescriptionInput", + "description": "Autogenerated input type of SetFriendlyDescription", + "fields": null, + "inputFields": [ + { + "name": "contextId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SetFriendlyDescriptionPayload", + "description": "Autogenerated return type of SetFriendlyDescription.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeFriendlyDescription", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "OutcomeFriendlyDescriptionType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SetModuleItemCompletionInput", + "description": "Autogenerated input type of SetModuleItemCompletion", + "fields": null, + "inputFields": [ + { + "name": "done", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "itemId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "moduleId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SetModuleItemCompletionPayload", + "description": "Autogenerated return type of SetModuleItemCompletion.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "moduleItem", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ModuleItem", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SetOverrideScoreInput", + "description": "Autogenerated input type of SetOverrideScore", + "fields": null, + "inputFields": [ + { + "name": "enrollmentId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingPeriodId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "overrideScore", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SetOverrideScorePayload", + "description": "Autogenerated return type of SetOverrideScore.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "grades", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Grades", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SetOverrideStatusInput", + "description": "Autogenerated input type of SetOverrideStatus", + "fields": null, + "inputFields": [ + { + "name": "customGradeStatusId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enrollmentId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingPeriodId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SetOverrideStatusPayload", + "description": "Autogenerated return type of SetOverrideStatus.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "grades", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Grades", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SetRubricSelfAssessmentInput", + "description": "Autogenerated input type of SetRubricSelfAssessment", + "fields": null, + "inputFields": [ + { + "name": "assignmentId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubricSelfAssessmentEnabled", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SetRubricSelfAssessmentPayload", + "description": "Autogenerated return type of SetRubricSelfAssessment.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SpeedGraderSettings", + "description": null, + "fields": [ + { + "name": "gradeByQuestion", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "StandardGradeStatus", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "color", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "StandardGradeStatusConnection", + "description": "The connection type for StandardGradeStatus.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "StandardGradeStatusEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "StandardGradeStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "StandardGradeStatusEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "StandardGradeStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "Sticker", + "description": "Valid sticker types for submissions", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "apple", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "basketball", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bell", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "book", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bookbag", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "briefcase", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bus", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "calendar", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "chem", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "design", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pencil", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "beaker", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paintbrush", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "computer", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "column", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pen", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tablet", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "telescope", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "calculator", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paperclip", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "composite_notebook", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "scissors", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ruler", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clock", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "globe", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "grad", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gym", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mail", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "microscope", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mouse", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "music", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notebook", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "page", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "panda1", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "panda2", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "panda3", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "panda4", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "panda5", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "panda6", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "panda7", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "panda8", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "panda9", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "presentation", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "science", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "science2", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "star", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tag", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tape", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "target", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "trophy", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "StreamSummaryItem", + "description": "An activity stream summary item", + "fields": [ + { + "name": "count", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notificationCategory", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unreadCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "SCALAR", + "name": "String", + "description": "Represents textual data as UTF-8 character sequences. This type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "StudentSummaryAnalytics", + "description": "basic information about a students activity in a course", + "fields": [ + { + "name": "pageViews", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "PageViewAnalysis", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "participations", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "PageViewAnalysis", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tardinessBreakdown", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "TardinessBreakdown", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SubAssignmentSubmission", + "description": null, + "fields": [ + { + "name": "assignmentId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customGradeStatusId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customGradeStatusId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enteredGrade", + "description": "the submission grade *before* late policy deductions were applied", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enteredScore", + "description": "the submission score *before* late policy deductions were applied", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "excused", + "description": "excused assignments are ignored when calculating grades", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "grade", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradeMatchesCurrentSubmission", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "publishedGrade", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "publishedScore", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "score", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "secondsLate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "statusTag", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SubmissionStatusTagType", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subAssignmentTag", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SubHeader", + "description": null, + "fields": [ + { + "name": "canUnpublish", + "description": "Whether the module item can be unpublished", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isLockedByMasterCourse", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "modules", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Module", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "published", + "description": "Whether the module item is published", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "ModuleItemInterface", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Submission", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anonymousId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignedAssessments", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "AssessmentRequest", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attachment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "File", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attachments", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "File", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attempt", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "body", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cachedDueDate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "commentsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "SubmissionCommentFilterInput", + "ofType": null + }, + "defaultValue": "{}", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": null, + "type": { + "kind": "ENUM", + "name": "SubmissionCommentsSortOrderType", + "ofType": null + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "includeDraftComments", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SubmissionCommentConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customGradeStatus", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customGradeStatusId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deductedPoints", + "description": "how many points are being deducted due to late policy", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enrollmentsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "EnrollmentConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enteredGrade", + "description": "the submission grade *before* late policy deductions were applied", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enteredScore", + "description": "the submission score *before* late policy deductions were applied", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "excused", + "description": "excused assignments are ignored when calculating grades", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "externalToolUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "extraAttempts", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "feedbackForCurrentAttempt", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "grade", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradeHidden", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradeMatchesCurrentSubmission", + "description": "was the grade given on the current submission (resubmission)", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradedAnonymously", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingPeriodId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingStatus", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "SubmissionGradingStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasPostableComments", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasUnreadRubricAssessment", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hideGradeFromStudent", + "description": "hide unpublished grades", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastCommentedByUserAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "late", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "latePolicyStatus", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "LatePolicyStatusType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaObject", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "MediaObject", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "missing", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "originalityData", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "JSON", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "posted", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "previewUrl", + "description": "This field is currently under development and its return value is subject to change.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "proxySubmitter", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "proxySubmitterId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "readState", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "redoRequest", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "resourceLinkLookupUuid", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubricAssessmentsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "SubmissionRubricAssessmentFilterInput", + "ofType": null + }, + "defaultValue": "{}", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RubricAssessmentConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "score", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "secondsLate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SubmissionState", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "statusTag", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SubmissionStatusTagType", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sticker", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "studentEnteredScore", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subAssignmentSubmissions", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SubAssignmentSubmission", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subAssignmentTag", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionCommentDownloadUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionDraft", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "SubmissionDraft", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionHistoriesConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "SubmissionHistoryFilterInput", + "ofType": null + }, + "defaultValue": "{}", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "orderBy", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "SubmissionHistoryOrder", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SubmissionHistoryConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionStatus", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionType", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "SubmissionType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submittedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "turnitinData", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TurnitinData", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unreadCommentCount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "wordCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "SubmissionInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SubmissionComment", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attachments", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "File", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attempt", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "author", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "canReply", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "comment", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "course", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Course", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "draft", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "htmlComment", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaCommentId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaObject", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "MediaObject", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "read", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SubmissionCommentConnection", + "description": "The connection type for SubmissionComment.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SubmissionCommentEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SubmissionComment", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SubmissionCommentEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "SubmissionComment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SubmissionCommentFilterInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "allComments", + "description": "If all of the comments, regardless of the submission attempt, should be returned.\nIf this is true, the for_attempt argument will be ignored.\n", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "forAttempt", + "description": "What submission attempt the comments should be returned for. If not specified,\nit will return the comments for the current submission or submission history.\n", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "peerReview", + "description": "Whether the current user is completing a peer review and should only see\ncomments authored by themselves.\n", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "SubmissionCommentsSortOrderType", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "asc", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "desc", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SubmissionConnection", + "description": "The connection type for Submission.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SubmissionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Submission", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SubmissionDraft", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "activeSubmissionType", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "DraftableSubmissionType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attachments", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "File", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "body", + "description": null, + "args": [ + { + "name": "rewriteUrls", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "externalTool", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "ExternalTool", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ltiLaunchUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaObject", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "MediaObject", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "meetsAssignmentCriteria", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "meetsBasicLtiLaunchCriteria", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "meetsMediaRecordingCriteria", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "meetsStudentAnnotationCriteria", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "meetsTextEntryCriteria", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "meetsUploadCriteria", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "meetsUrlCriteria", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "resourceLinkLookupUuid", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionAttempt", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SubmissionEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Submission", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SubmissionFilterInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "states", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SubmissionState", + "ofType": null + } + } + }, + "defaultValue": "[submitted, pending_review, graded]", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sectionIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dueBetween", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "DateTimeRange", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradedSince", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submittedSince", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedSince", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "SubmissionGradingStatus", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "needs_grading", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "excused", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "needs_review", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "graded", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SubmissionHistory", + "description": null, + "fields": [ + { + "name": "anonymousId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignedAssessments", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "AssessmentRequest", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attachment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "File", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attachments", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "File", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attempt", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "body", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cachedDueDate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "commentsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "SubmissionCommentFilterInput", + "ofType": null + }, + "defaultValue": "{}", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": null, + "type": { + "kind": "ENUM", + "name": "SubmissionCommentsSortOrderType", + "ofType": null + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "includeDraftComments", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SubmissionCommentConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customGradeStatus", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customGradeStatusId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deductedPoints", + "description": "how many points are being deducted due to late policy", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enteredGrade", + "description": "the submission grade *before* late policy deductions were applied", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enteredScore", + "description": "the submission score *before* late policy deductions were applied", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "excused", + "description": "excused assignments are ignored when calculating grades", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "externalToolUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "extraAttempts", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "feedbackForCurrentAttempt", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "grade", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradeHidden", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradeMatchesCurrentSubmission", + "description": "was the grade given on the current submission (resubmission)", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradedAnonymously", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingStatus", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "SubmissionGradingStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasPostableComments", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasUnreadRubricAssessment", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hideGradeFromStudent", + "description": "hide unpublished grades", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastCommentedByUserAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "late", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "latePolicyStatus", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "LatePolicyStatusType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaObject", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "MediaObject", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "missing", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "originalityData", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "JSON", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "posted", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "previewUrl", + "description": "This field is currently under development and its return value is subject to change.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "proxySubmitter", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "proxySubmitterId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "redoRequest", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "resourceLinkLookupUuid", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rootId", + "description": "The canvas legacy id of the root submission this history belongs to\n", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubricAssessmentsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "SubmissionRubricAssessmentFilterInput", + "ofType": null + }, + "defaultValue": "{}", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RubricAssessmentConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "score", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "secondsLate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SubmissionState", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "statusTag", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SubmissionStatusTagType", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sticker", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subAssignmentSubmissions", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SubAssignmentSubmission", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionCommentDownloadUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionDraft", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "SubmissionDraft", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionStatus", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionType", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "SubmissionType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submittedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "turnitinData", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TurnitinData", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unreadCommentCount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "wordCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "SubmissionInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SubmissionHistoryConnection", + "description": "The connection type for SubmissionHistory.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SubmissionHistoryEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SubmissionHistory", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SubmissionHistoryEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "SubmissionHistory", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SubmissionHistoryFilterInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "states", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SubmissionState", + "ofType": null + } + } + }, + "defaultValue": "[deleted, graded, pending_review, submitted, ungraded, unsubmitted]", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "includeCurrentSubmission", + "description": "If the most current submission should be included in the submission\nhistory results. Defaults to true.\n", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "true", + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SubmissionHistoryOrder", + "description": "Specify a sort for the results", + "fields": null, + "inputFields": [ + { + "name": "direction", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "OrderDirection", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "field", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SubmissionHistoryOrderField", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "SubmissionHistoryOrderField", + "description": "The submission history field to sort by", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "attempt", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INTERFACE", + "name": "SubmissionInterface", + "description": "Types for submission or submission history", + "fields": [ + { + "name": "anonymousId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignedAssessments", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "AssessmentRequest", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attachment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "File", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attachments", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "File", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "attempt", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "body", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cachedDueDate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "commentsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "SubmissionCommentFilterInput", + "ofType": null + }, + "defaultValue": "{}", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": null, + "type": { + "kind": "ENUM", + "name": "SubmissionCommentsSortOrderType", + "ofType": null + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "includeDraftComments", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SubmissionCommentConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customGradeStatus", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customGradeStatusId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deductedPoints", + "description": "how many points are being deducted due to late policy", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enteredGrade", + "description": "the submission grade *before* late policy deductions were applied", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enteredScore", + "description": "the submission score *before* late policy deductions were applied", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "excused", + "description": "excused assignments are ignored when calculating grades", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "externalToolUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "extraAttempts", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "feedbackForCurrentAttempt", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "grade", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradeHidden", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradeMatchesCurrentSubmission", + "description": "was the grade given on the current submission (resubmission)", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradedAnonymously", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingStatus", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "SubmissionGradingStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasPostableComments", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasUnreadRubricAssessment", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hideGradeFromStudent", + "description": "hide unpublished grades", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastCommentedByUserAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "late", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "latePolicyStatus", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "LatePolicyStatusType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mediaObject", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "MediaObject", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "missing", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "originalityData", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "JSON", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "posted", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "previewUrl", + "description": "This field is currently under development and its return value is subject to change.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "proxySubmitter", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "proxySubmitterId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "redoRequest", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "resourceLinkLookupUuid", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubricAssessmentsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "SubmissionRubricAssessmentFilterInput", + "ofType": null + }, + "defaultValue": "{}", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RubricAssessmentConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "score", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "secondsLate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SubmissionState", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "statusTag", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SubmissionStatusTagType", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sticker", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subAssignmentSubmissions", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SubAssignmentSubmission", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionCommentDownloadUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionDraft", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "SubmissionDraft", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionStatus", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionType", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "SubmissionType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submittedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "turnitinData", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TurnitinData", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unreadCommentCount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "wordCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Submission", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SubmissionHistory", + "ofType": null + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SubmissionOrderCriteria", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "direction", + "description": null, + "type": { + "kind": "ENUM", + "name": "OrderDirection", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "field", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SubmissionOrderField", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "SubmissionOrderField", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "_id", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradedAt", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SubmissionRubricAssessmentFilterInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "forAttempt", + "description": "What submission attempt the rubric assessment should be returned for. If not\nspecified, it will return the rubric assessment for the current submisssion\nor submission history.\n", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": "null", + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SubmissionSearchFilterInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "applyGradebookEnrollmentFilters", + "description": "Filters submissions for deactivated and concluded users based on the calling user's\n'Show -\u003e Inactive Enrollments' and 'Show -\u003e Concluded Enrollments' settings in the Gradebook.\nWhen true, this filter takes precedence over the include_concluded and include_deactivated filters.\n", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "applyGradebookGroupFilter", + "description": "Filters submissions for users in a specific group applied in the Gradebook.\n", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "includeUnsubmitted", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "representativesOnly", + "description": "For group assignments, include submissions for group representatives only.\nHas no effect on non-group assignments or group assignments where students\nare being graded individually.\n", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sectionIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "states", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SubmissionState", + "ofType": null + } + } + }, + "defaultValue": "[submitted, pending_review, graded]", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enrollmentTypes", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "EnrollmentType", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "includeConcluded", + "description": "Include submissions for concluded students.\n", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "includeDeactivated", + "description": "Include submissions for deactivated students.\n", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userSearch", + "description": "The partial name or full ID of the users to match and return in the\nresults list. Must be at least 3 characters.\nQueries by administrative users will search on SIS ID, login ID, name, or email\naddress; non-administrative queries will only be compared against name.\n", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userId", + "description": "Return only submissions related to the given user_id\nThere is no character restriction on this field\n", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anonymousId", + "description": "Return only submissions related to the given anonymous_id\nThere is no character restriction on this field\n", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "late", + "description": "Limit results to submissions that are late", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "scoredLessThan", + "description": "Limit results to submissions that scored below the specified value", + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "scoredMoreThan", + "description": "Limit results to submissions that scored above the specified value", + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingStatus", + "description": "Limit results by grading status", + "type": { + "kind": "ENUM", + "name": "SubmissionGradingStatus", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SubmissionSearchOrder", + "description": "Specify a sort for the results", + "fields": null, + "inputFields": [ + { + "name": "direction", + "description": null, + "type": { + "kind": "ENUM", + "name": "OrderDirection", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "field", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SubmissionSearchOrderField", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "SubmissionSearchOrderField", + "description": "The user or submission field to sort by", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "username", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "score", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submitted_at", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "SubmissionState", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "submitted", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unsubmitted", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pending_review", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "graded", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ungraded", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleted", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "SubmissionStatusTagType", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "custom", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "excused", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "missing", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "late", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "extended", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "none", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "SubmissionType", + "description": "Types of submissions an assignment accepts", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "attendance", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "basic_lti_launch", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussion_topic", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "external_tool", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "media_recording", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "none", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "not_graded", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "on_paper", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "online_quiz", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "online_text_entry", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "online_upload", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "online_url", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "student_annotation", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "wiki_page", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "SubscribeToDiscussionTopicInput", + "description": "Autogenerated input type of SubscribeToDiscussionTopic", + "fields": null, + "inputFields": [ + { + "name": "discussionTopicId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscribed", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "SubscribeToDiscussionTopicPayload", + "description": "Autogenerated return type of SubscribeToDiscussionTopic.", + "fields": [ + { + "name": "discussionTopic", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "INPUT_OBJECT", - "name": "MarkSubmissionCommentsReadInput", + "kind": "OBJECT", + "name": "ValidationError", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "MarkSubmissionCommentsReadPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "moveOutcomeLinks", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "MoveOutcomeLinksInput", + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "TardinessBreakdown", + "description": "statistics based on timeliness of student submissions", + "fields": [ + { + "name": "late", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "missing", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "total", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "Term", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "coursesConnection", + "description": "courses for this term", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "MoveOutcomeLinksPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "postAssignmentGrades", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "PostAssignmentGradesInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "PostAssignmentGradesPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "postAssignmentGradesForSections", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "PostAssignmentGradesForSectionsInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "PostAssignmentGradesForSectionsPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "setAssignmentPostPolicy", - "description": "Sets the post policy for the assignment.\n", - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "SetAssignmentPostPolicyInput", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CourseConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "endAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sisId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sisTermId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "startAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INTERFACE", + "name": "Timestamped", + "description": "Contains timestamp metadata", + "fields": [ + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { "kind": "OBJECT", - "name": "SetAssignmentPostPolicyPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "setCoursePostPolicy", - "description": "Sets the post policy for the course, with an option to override and delete\nexisting assignment post policies.\n", - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "SetCoursePostPolicyInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "AccountDomain", + "ofType": null + }, + { "kind": "OBJECT", - "name": "SetCoursePostPolicyPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "setFriendlyDescription", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "SetFriendlyDescriptionInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "AccountDomainLookup", + "ofType": null + }, + { "kind": "OBJECT", - "name": "SetFriendlyDescriptionPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "setModuleItemCompletion", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "SetModuleItemCompletionInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "AssessmentRequest", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "AssignmentGroup", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "AssignmentOverride", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "CommentBankItem", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "CommunicationChannel", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ContentTag", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Course", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "DiscussionEntry", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "DiscussionEntryDraft", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "DiscussionEntryVersion", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Enrollment", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ExternalTool", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ExternalUrl", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "File", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "GradingPeriod", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "GradingPeriodGroup", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Group", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "GroupMembership", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "InboxSettings", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "InternalSetting", + "ofType": null + }, + { "kind": "OBJECT", - "name": "SetModuleItemCompletionPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "setOverrideScore", - "description": "Sets the overridden final score for the associated enrollment, optionally limited to a specific\ngrading period. This will supersede the computed final score/grade if present.\n", - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "SetOverrideScoreInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "LearningOutcome", + "ofType": null + }, + { "kind": "OBJECT", - "name": "SetOverrideScorePayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "subscribeToDiscussionTopic", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "SubscribeToDiscussionTopicInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "Module", + "ofType": null + }, + { "kind": "OBJECT", - "name": "SubscribeToDiscussionTopicPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateAccountDomainLookup", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "UpdateAccountDomainLookupInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "ModuleExternalTool", + "ofType": null + }, + { "kind": "OBJECT", - "name": "UpdateAccountDomainLookupPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateAssignment", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "UpdateAssignmentInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "ModuleItem", + "ofType": null + }, + { "kind": "OBJECT", - "name": "UpdateAssignmentPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateCommentBankItem", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "UpdateCommentBankItemInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "Notification", + "ofType": null + }, + { "kind": "OBJECT", - "name": "UpdateCommentBankItemPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateConversationParticipants", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "UpdateConversationParticipantsInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "NotificationPolicy", + "ofType": null + }, + { "kind": "OBJECT", - "name": "UpdateConversationParticipantsPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateDiscussionEntriesReadState", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "UpdateDiscussionEntriesReadStateInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "OutcomeAlignment", + "ofType": null + }, + { "kind": "OBJECT", - "name": "UpdateDiscussionEntriesReadStatePayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateDiscussionEntry", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "UpdateDiscussionEntryInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "OutcomeFriendlyDescriptionType", + "ofType": null + }, + { "kind": "OBJECT", - "name": "UpdateDiscussionEntryPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateDiscussionEntryParticipant", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "UpdateDiscussionEntryParticipantInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "Page", + "ofType": null + }, + { "kind": "OBJECT", - "name": "UpdateDiscussionEntryParticipantPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateDiscussionReadState", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "UpdateDiscussionReadStateInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "Progress", + "ofType": null + }, + { "kind": "OBJECT", - "name": "UpdateDiscussionReadStatePayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateDiscussionThreadReadState", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "UpdateDiscussionThreadReadStateInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "Quiz", + "ofType": null + }, + { "kind": "OBJECT", - "name": "UpdateDiscussionThreadReadStatePayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateDiscussionTopic", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "UpdateDiscussionTopicInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "Section", + "ofType": null + }, + { "kind": "OBJECT", - "name": "UpdateDiscussionTopicPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateIsolatedViewDeeplyNestedAlert", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "UpdateIsolatedViewDeeplyNestedAlertInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "Submission", + "ofType": null + }, + { "kind": "OBJECT", - "name": "UpdateIsolatedViewDeeplyNestedAlertPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateLearningOutcome", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "UpdateLearningOutcomeInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "SubmissionComment", + "ofType": null + }, + { "kind": "OBJECT", - "name": "UpdateLearningOutcomePayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateLearningOutcomeGroup", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "UpdateLearningOutcomeGroupInput", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { + "name": "SubmissionHistory", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "UNION", + "name": "TurnitinContext", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "File", + "ofType": null + }, + { "kind": "OBJECT", - "name": "UpdateLearningOutcomeGroupPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateNotificationPreferences", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { + "name": "Submission", + "ofType": null + } + ], + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "TurnitinData", + "description": null, + "fields": [ + { + "name": "reportUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "score", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "target", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "UNION", + "name": "TurnitinContext", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "SCALAR", + "name": "URL", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateAccountDomainLookupInput", + "description": "Autogenerated input type of UpdateAccountDomainLookup", + "fields": null, + "inputFields": [ + { + "name": "accountDomainId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "accountDomainLookupId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authenticationProvider", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateAccountDomainLookupPayload", + "description": "Autogenerated return type of UpdateAccountDomainLookup.", + "fields": [ + { + "name": "accountDomainLookup", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "AccountDomainLookup", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "INPUT_OBJECT", - "name": "UpdateNotificationPreferencesInput", + "kind": "OBJECT", + "name": "ValidationError", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "UpdateNotificationPreferencesPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateOutcomeCalculationMethod", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateAssignmentInput", + "description": "Autogenerated input type of UpdateAssignment", + "fields": null, + "inputFields": [ + { + "name": "allowedAttempts", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allowedExtensions", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "INPUT_OBJECT", - "name": "UpdateOutcomeCalculationMethodInput", + "kind": "SCALAR", + "name": "String", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "UpdateOutcomeCalculationMethodPayload", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updateOutcomeProficiency", - "description": null, - "args": [ - { - "name": "input", - "description": null, - "type": { + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anonymousGrading", + "description": "requires anonymous_marking course feature to be set to true", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anonymousInstructorAnnotations", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentGroupId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentOverrides", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "INPUT_OBJECT", - "name": "UpdateOutcomeProficiencyInput", + "name": "AssignmentOverrideCreateOrUpdate", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "UpdateOutcomeProficiencyPayload", - "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MutationLog", - "description": null, - "fields": [ - { - "name": "assetString", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "dueAt", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mutationId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "forCheckpoints", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mutationName", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "params", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "JSON", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "realUser", - "description": "If the mutation was performed by a user masquerading as another user,\nthis field returns the \"real\" (logged-in) user.\n", - "args": [], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "timestamp", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "user", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MutationLogConnection", - "description": "The connection type for MutationLog.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "MutationLogEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "MutationLog", + { + "name": "gradeGroupStudentsIndividually", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MutationLogEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "MutationLog", - "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INTERFACE", - "name": "Node", - "description": "An object with an ID.", - "fields": [ - { - "name": "id", - "description": "ID of the object.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "gradingStandardId", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": [ - { - "kind": "OBJECT", - "name": "Account", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "AssignmentGroup", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "AssignmentOverride", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "CommentBankItem", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "CommunicationChannel", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ContentTag", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Conversation", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Course", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Discussion", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "DiscussionEntry", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Enrollment", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "File", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "GradingPeriod", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Group", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "GroupSet", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "LearningOutcome", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "LearningOutcomeGroup", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "MediaObject", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "MessageableContext", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "MessageableUser", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Module", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ModuleItem", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Notification", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "NotificationPolicy", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "OutcomeCalculationMethod", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "OutcomeFriendlyDescriptionType", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "OutcomeProficiency", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Page", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "PostPolicy", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Progress", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Quiz", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Rubric", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Section", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Submission", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Term", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "User", - "ofType": null - } - ] - }, - { - "kind": "ENUM", - "name": "NodeType", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "Account", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Assignment", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "AssignmentGroup", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Conversation", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Course", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Discussion", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "DiscussionEntry", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Enrollment", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "File", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "GradingPeriod", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Group", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "GroupSet", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "LearningOutcomeGroup", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "MediaObject", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Module", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ModuleItem", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "OutcomeCalculationMethod", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "OutcomeProficiency", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Page", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "PostPolicy", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Progress", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Rubric", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Section", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Submission", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Term", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "User", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Noop", - "description": "A descriptive tag that doesn't link the assignment to a set", - "fields": [ - { - "name": "_id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gradingType", + "description": null, + "type": { + "kind": "ENUM", + "name": "GradingType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupCategoryId", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Notification", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "groupSetId", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "category", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "categoryDescription", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "categoryDisplayName", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lockAt", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "workflowState", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "NotificationCategoryType", - "description": "The categories that a notification can belong to", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "Account_Notification", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Added_To_Conversation", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "All_Submissions", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Announcement", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Announcement_Created_By_You", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Appointment_Availability", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Appointment_Cancelations", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Appointment_Signups", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Blueprint", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Calendar", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Content_Link_Error", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Conversation_Created", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Conversation_Message", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Course_Content", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Discussion", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "DiscussionEntry", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "DiscussionMention", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ReportedReply", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Due_Date", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Files", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Grading", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Grading_Policies", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Invitation", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Late_Grading", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Membership_Update", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Other", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Recording_Ready", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Student_Appointment_Signups", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Submission_Comment", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "NotificationFrequencyType", - "description": "Frequency that notifications can be delivered on", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "immediately", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "daily", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "weekly", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "never", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "NotificationPolicy", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "moderatedGrading", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignmentModeratedGradingUpdate", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "moduleIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "omitFromFinalGrade", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "communicationChannelId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "onlyVisibleToOverrides", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "peerReviews", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignmentPeerReviewsUpdate", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "frequency", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pointsPossible", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Float", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "notification", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Notification", - "ofType": null + { + "name": "position", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "NotificationPreferences", - "description": null, - "fields": [ - { - "name": "channels", - "description": null, - "args": [ - { - "name": "channelId", - "description": null, - "type": { + { + "name": "postToSis", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": null, + "type": { + "kind": "ENUM", + "name": "AssignmentState", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionTypes", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SubmissionType", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unlockAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null - }, - "defaultValue": "null" - } - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateAssignmentPayload", + "description": "Autogenerated return type of UpdateAssignment.", + "fields": [ + { + "name": "assignment", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Assignment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateCommentBankItemInput", + "description": "Autogenerated input type of UpdateCommentBankItem", + "fields": null, + "inputFields": [ + { + "name": "comment", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "CommunicationChannel", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "readPrivacyNoticeDate", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sendObservedNamesInNotifications", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sendScoresInEmails", - "description": null, - "args": [ - { - "name": "courseId", - "description": null, - "type": { "kind": "SCALAR", "name": "ID", "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateCommentBankItemPayload", + "description": "Autogenerated return type of UpdateCommentBankItem.", + "fields": [ + { + "name": "commentBankItem", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "CommentBankItem", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateConversationParticipantsInput", + "description": "Autogenerated input type of UpdateConversationParticipants", + "fields": null, + "inputFields": [ + { + "name": "conversationIds", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "NotificationPreferencesContextType", - "description": "Context types that can be associated with notification preferences", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "Course", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Account", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "OnlineSubmissionType", - "description": "Types that can be submitted online", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "basic_lti_launch", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "student_annotation", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "media_recording", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "online_text_entry", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "online_upload", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "online_url", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "OrderDirection", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "ascending", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "descending", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "OutcomeCalculationMethod", - "description": "Customized calculation method", - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "starred", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "calculationInt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "calculationMethod", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscribed", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextType", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "workflowState", + "description": null, + "type": { "kind": "SCALAR", "name": "String", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateConversationParticipantsPayload", + "description": "Autogenerated return type of UpdateConversationParticipants.", + "fields": [ + { + "name": "conversationParticipants", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ConversationParticipant", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionEntriesReadStateInput", + "description": "Autogenerated input type of UpdateDiscussionEntriesReadState", + "fields": null, + "inputFields": [ + { + "name": "discussionEntryIds", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "read", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateDiscussionEntriesReadStatePayload", + "description": "Autogenerated return type of UpdateDiscussionEntriesReadState.", + "fields": [ + { + "name": "discussionEntries", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DiscussionEntry", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionEntryInput", + "description": "Autogenerated input type of UpdateDiscussionEntry", + "fields": null, + "inputFields": [ + { + "name": "discussionEntryId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "locked", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "OutcomeFriendlyDescriptionType", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "fileId", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextType", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "message", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "learningOutcomeId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "workflowState", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "OutcomeProficiency", - "description": "Customized proficiency ratings", - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "quotedEntryId", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "removeAttachment", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionEntryParticipantInput", + "description": "Autogenerated input type of UpdateDiscussionEntryParticipant", + "fields": null, + "inputFields": [ + { + "name": "discussionEntryId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "contextType", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "forcedReadState", + "description": null, + "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rating", + "description": null, + "type": { + "kind": "ENUM", + "name": "RatingInputType", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "locked", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "read", + "description": null, + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "proficiencyRatingsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reportType", + "description": null, + "type": { + "kind": "ENUM", + "name": "ReportType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateDiscussionEntryParticipantPayload", + "description": "Autogenerated return type of UpdateDiscussionEntryParticipant.", + "fields": [ + { + "name": "discussionEntry", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DiscussionEntry", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateDiscussionEntryPayload", + "description": "Autogenerated return type of UpdateDiscussionEntry.", + "fields": [ + { + "name": "discussionEntry", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "DiscussionEntry", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionExpandedInput", + "description": "Autogenerated input type of UpdateDiscussionExpanded", + "fields": null, + "inputFields": [ + { + "name": "discussionTopicId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expanded", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null - }, - "defaultValue": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateDiscussionExpandedPayload", + "description": "Autogenerated return type of UpdateDiscussionExpanded.", + "fields": [ + { + "name": "discussionTopic", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionReadStateInput", + "description": "Autogenerated input type of UpdateDiscussionReadState", + "fields": null, + "inputFields": [ + { + "name": "discussionTopicId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "read", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "Boolean", "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "ProficiencyRatingConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "OutcomeProficiencyRatingCreate", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "color", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "description", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "mastery", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "points", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Page", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "modules", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateDiscussionReadStatePayload", + "description": "Autogenerated return type of UpdateDiscussionReadState.", + "fields": [ + { + "name": "discussionTopic", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", - "name": "Module", + "name": "Discussion", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "title", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "ModuleItemInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "PageInfo", - "description": "Information about pagination in a connection.", - "fields": [ - { - "name": "endCursor", - "description": "When paginating forwards, the cursor to continue.", - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "hasNextPage", - "description": "When paginating forwards, are there more items?", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "hasPreviousPage", - "description": "When paginating backwards, are there more items?", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "startCursor", - "description": "When paginating backwards, the cursor to continue.", - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "PageViewAnalysis", - "description": null, - "fields": [ - { - "name": "level", - "description": "This number (0-3) is intended to give an idea of how the student is doing relative to others in the course", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "max", - "description": "The maximum number of views/participations in this course", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "total", - "description": "The number of views/participations this student has", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "PeerReviews", - "description": "Settings for Peer Reviews on an Assignment", - "fields": [ - { - "name": "anonymousReviews", - "description": "Boolean representing whether or not peer reviews are anonymous", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "automaticReviews", - "description": "Boolean indicating peer reviews are assigned automatically. If false, the teacher is expected to manually assign peer reviews.", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "count", - "description": "Integer representing the amount of reviews each user is assigned.", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "dueAt", - "description": "Date and Time representing when the peer reviews are due", - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "enabled", - "description": "Boolean indicating if peer reviews are required for this assignment", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "intraReviews", - "description": "Boolean representing whether or not members from within the same group on a group assignment can be assigned to peer review their own group's work", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "PostAssignmentGradesForSectionsInput", - "description": "Autogenerated input type of PostAssignmentGradesForSections", - "fields": null, - "inputFields": [ - { - "name": "assignmentId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "sectionIds", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "ID", + "kind": "OBJECT", + "name": "ValidationError", "ofType": null } } - } - }, - "defaultValue": null - }, - { - "name": "gradedOnly", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "PostAssignmentGradesForSectionsPayload", - "description": "Autogenerated return type of PostAssignmentGradesForSections", - "fields": [ - { - "name": "assignment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionSortOrderInput", + "description": "Autogenerated input type of UpdateDiscussionSortOrder", + "fields": null, + "inputFields": [ + { + "name": "discussionTopicId", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", + "kind": "SCALAR", + "name": "ID", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "progress", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Progress", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sections", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "DiscussionSortOrderType", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateDiscussionSortOrderPayload", + "description": "Autogenerated return type of UpdateDiscussionSortOrder.", + "fields": [ + { + "name": "discussionTopic", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", - "name": "Section", + "name": "Discussion", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "PostAssignmentGradesInput", - "description": "Autogenerated input type of PostAssignmentGrades", - "fields": null, - "inputFields": [ - { - "name": "assignmentId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "sectionIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionThreadReadStateInput", + "description": "Autogenerated input type of UpdateDiscussionThreadReadState", + "fields": null, + "inputFields": [ + { + "name": "discussionEntryId", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { @@ -21301,1454 +45697,804 @@ "name": "ID", "ofType": null } - } - }, - "defaultValue": null - }, - { - "name": "onlyStudentIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "read", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null } - } - }, - "defaultValue": null - }, - { - "name": "skipStudentIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateDiscussionThreadReadStatePayload", + "description": "Autogenerated return type of UpdateDiscussionThreadReadState.", + "fields": [ + { + "name": "discussionEntry", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "ID", + "kind": "OBJECT", + "name": "DiscussionEntry", "ofType": null } - } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateDiscussionTopicInput", + "description": "Autogenerated input type of UpdateDiscussionTopic", + "fields": null, + "inputFields": [ + { + "name": "allowRating", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "checkpoints", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DiscussionCheckpoints", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "delayedPostAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expanded", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expandedLocked", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fileId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groupCategoryId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "gradedOnly", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + { + "name": "lockAt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "PostAssignmentGradesPayload", - "description": "Autogenerated return type of PostAssignmentGrades", - "fields": [ - { - "name": "assignment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null + { + "name": "locked", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } + { + "name": "message", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "progress", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Progress", - "ofType": null + { + "name": "onlyGradersCanRate", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sections", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Section", - "ofType": null - } - } + { + "name": "onlyVisibleToOverrides", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "PostPolicy", - "description": "A PostPolicy sets the policy for whether a Submission's grades are posted\nautomatically or manually. A PostPolicy can be set at the Course and/or\nAssignment level.\n", - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "podcastEnabled", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assignment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null + { + "name": "podcastHasStudentPosts", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "course", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Course", + { + "name": "published", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": "ID of the object.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "requireInitialPost", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "postManually", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "PostPolicyConnection", - "description": "The connection type for PostPolicy.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PostPolicyEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PostPolicy", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": null, + "type": { + "kind": "ENUM", + "name": "DiscussionSortOrderType", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "PostPolicyEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "PostPolicy", - "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ProficiencyRating", - "description": "Customized proficiency rating", - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "sortOrderLocked", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "color", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mastery", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "points", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ProficiencyRatingConnection", - "description": "The connection type for ProficiencyRating.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ProficiencyRatingEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ProficiencyRating", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "specificSections", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ProficiencyRatingEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "ProficiencyRating", - "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Progress", - "description": "Returns completion status and progress information about an asynchronous job", - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "title", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "completion", - "description": "percent completed", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "context", - "description": null, - "args": [], - "type": { - "kind": "UNION", - "name": "ProgressContext", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "todoDate", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "message", - "description": "details about the job", - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "state", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anonymousState", + "description": null, + "type": { "kind": "ENUM", - "name": "ProgressState", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "tag", - "description": "the type of operation", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "UNION", - "name": "ProgressContext", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": [ - { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Course", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "File", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "GroupSet", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "User", - "ofType": null - } - ] - }, - { - "kind": "ENUM", - "name": "ProgressState", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "queued", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "running", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "completed", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "failed", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Query", - "description": null, - "fields": [ - { - "name": "account", - "description": null, - "args": [ - { - "name": "id", - "description": "a graphql or legacy id", - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null + "name": "DiscussionTopicAnonymousStateType", + "ofType": null }, - { - "name": "sisId", - "description": "a id from the original SIS system", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "Account", - "ofType": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "allCourses", - "description": "All courses viewable by the current user", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + { + "name": "assignment", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignmentUpdate", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussionTopicId", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "Course", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assignment", - "description": null, - "args": [ - { - "name": "id", - "description": "a graphql or legacy id", - "type": { "kind": "SCALAR", "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "sisId", - "description": "an id from the original SIS system", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assignmentGroup", - "description": null, - "args": [ - { - "name": "id", - "description": "a graphql or legacy id", - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussionType", + "description": null, + "type": { + "kind": "ENUM", + "name": "DiscussionTopicDiscussionType", + "ofType": null }, - { - "name": "sisId", - "description": "an id from the original SIS system", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "AssignmentGroup", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "auditLogs", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "AuditLogs", - "ofType": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "course", - "description": null, - "args": [ - { - "name": "id", - "description": "a graphql or legacy id, preference for search is given to this id", - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null + { + "name": "removeAttachment", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "sisId", - "description": "a id from the original SIS system", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "Course", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "learningOutcomeGroup", - "description": "LearningOutcomeGroup", - "args": [ - { - "name": "id", - "description": "a graphql or legacy id", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "LearningOutcomeGroup", - "ofType": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "legacyNode", - "description": "Fetches an object given its type and legacy ID", - "args": [ - { - "name": "_id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null + { + "name": "notifyUsers", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "type", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "NodeType", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "moduleItem", - "description": "ModuleItem", - "args": [ - { - "name": "id", - "description": "a graphql or legacy id", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "ModuleItem", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "Fetches an object given its ID.", - "args": [ - { - "name": "id", - "description": "ID of the object.", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcomeCalculationMethod", - "description": "OutcomeCalculationMethod", - "args": [ - { - "name": "id", - "description": "a graphql or legacy id", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "OutcomeCalculationMethod", - "ofType": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcomeProficiency", - "description": "OutcomeProficiency", - "args": [ - { - "name": "id", - "description": "a graphql or legacy id", - "type": { + { + "name": "setCheckpoints", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ungradedDiscussionOverrides", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "ID", + "kind": "INPUT_OBJECT", + "name": "AssignmentOverrideCreateOrUpdate", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "OutcomeProficiency", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submission", - "description": null, - "args": [ - { - "name": "id", - "description": "a graphql or legacy id", - "type": { + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateDiscussionTopicPayload", + "description": "Autogenerated return type of UpdateDiscussionTopic.", + "fields": [ + { + "name": "discussionTopic", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "ID", + "kind": "OBJECT", + "name": "ValidationError", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "Submission", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "term", - "description": null, - "args": [ - { - "name": "id", - "description": "a graphql or legacy id", - "type": { + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateGradebookGroupFilterInput", + "description": "Autogenerated input type of UpdateGradebookGroupFilter", + "fields": null, + "inputFields": [ + { + "name": "anonymousId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "sisId", - "description": "an id from the original SIS system", - "type": { + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "Term", - "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateGradebookGroupFilterPayload", + "description": "Autogenerated return type of UpdateGradebookGroupFilter.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Quiz", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "groupName", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reasonForChange", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "modules", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateInternalSettingInput", + "description": "Autogenerated input type of UpdateInternalSetting", + "fields": null, + "inputFields": [ + { + "name": "internalSettingId", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "Module", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "ModuleItemInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "RatingInputType", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "not_liked", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "liked", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Recipients", - "description": null, - "fields": [ - { - "name": "contextsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "value", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateInternalSettingPayload", + "description": "Autogenerated return type of UpdateInternalSetting.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "MessageableContextConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "usersConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "internalSetting", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "InternalSetting", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateLearningOutcomeGroupInput", + "description": "Autogenerated input type of UpdateLearningOutcomeGroup", + "fields": null, + "inputFields": [ + { + "name": "description", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "MessageableUserConnection", - "ofType": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "ReportType", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "inappropriate", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "offensive", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "other", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Rubric", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "parentOutcomeGroupId", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "criteria", - "description": "The different criteria that makes up this rubric\n", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "vendorGuid", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateLearningOutcomeGroupPayload", + "description": "Autogenerated return type of UpdateLearningOutcomeGroup.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { "kind": "LIST", "name": null, "ofType": { @@ -22756,3042 +46502,1114 @@ "name": null, "ofType": { "kind": "OBJECT", - "name": "RubricCriterion", + "name": "ValidationError", "ofType": null } } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "freeFormCriterionComments", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "learningOutcomeGroup", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "LearningOutcomeGroup", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateLearningOutcomeInput", + "description": "Autogenerated input type of UpdateLearningOutcome", + "fields": null, + "inputFields": [ + { + "name": "calculationInt", + "description": null, + "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "Int", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "hideScoreTotal", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "calculationMethod", + "description": null, + "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "description", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pointsPossible", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "title", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "RubricAssessment", - "description": "An assessment for a rubric", - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "displayName", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "artifactAttempt", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "masteryPoints", + "description": null, + "type": { "kind": "SCALAR", - "name": "Int", + "name": "Float", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assessmentRatings", - "description": "The assessments for the individual criteria in this rubric\n", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ratings", + "description": null, + "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "RubricAssessmentRating", + "kind": "INPUT_OBJECT", + "name": "ProficiencyRatingInput", "ofType": null } } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assessmentType", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "AssessmentType", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assessor", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rubricAssociation", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "RubricAssociation", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "score", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "user", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "RubricAssessmentConnection", - "description": "The connection type for RubricAssessment.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "RubricAssessmentEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "RubricAssessment", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "RubricAssessmentEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "RubricAssessment", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "RubricAssessmentRating", - "description": "An assessment for a specific criteria in a rubric", - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "comments", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "commentsHtml", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "criterion", - "description": "The rubric criteria that this assessment is for\n", - "args": [], - "type": { - "kind": "OBJECT", - "name": "RubricCriterion", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcome", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "LearningOutcome", - "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "points", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "RubricAssociation", - "description": "How a rubric is being used in a context", - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "hidePoints", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "hideScoreTotal", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "useForGrading", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "RubricCriterion", - "description": "Individual criteria for a rubric", - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "vendorGuid", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "criterionUseRange", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ignoreForScoring", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "longDescription", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "masteryPoints", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcome", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "LearningOutcome", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "points", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ratings", - "description": "The possible ratings available for this criterion\n", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "RubricRating", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "RubricCriterionInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "masteryPoints", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "ratings", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "RubricCriterionRatingInput", - "ofType": null - } - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "RubricCriterionRatingInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "description", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "points", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateLearningOutcomePayload", + "description": "Autogenerated return type of UpdateLearningOutcome.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "learningOutcome", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "LearningOutcome", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateMyInboxSettingsInput", + "description": "Autogenerated input type of UpdateMyInboxSettings", + "fields": null, + "inputFields": [ + { + "name": "outOfOfficeFirstDate", + "description": null, + "type": { "kind": "SCALAR", - "name": "Int", + "name": "String", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "RubricRating", - "description": "Possible rating for a rubric criterion", - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "outOfOfficeLastDate", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "longDescription", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "points", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outOfOfficeMessage", + "description": null, + "type": { "kind": "SCALAR", - "name": "Float", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Section", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outOfOfficeSubject", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "signature", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sisId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SectionConnection", - "description": "The connection type for Section.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "SectionEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "useOutOfOffice", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "useSignature", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateMyInboxSettingsPayload", + "description": "Autogenerated return type of UpdateMyInboxSettings.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "myInboxSettings", + "description": null, + "args": [], + "type": { "kind": "OBJECT", - "name": "Section", + "name": "InboxSettings", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SectionEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "Section", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "SelfSignupPolicy", - "description": "Determines if/how a student may join a group. A student can belong to\nonly one group per group set at a time.\n", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "enabled", - "description": "students may join any group", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "restricted", - "description": "students may join a group in their section", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "disabled", - "description": "self signup is not allowed", - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "SetAssignmentPostPolicyInput", - "description": "Autogenerated input type of SetAssignmentPostPolicy", - "fields": null, - "inputFields": [ - { - "name": "assignmentId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateNotificationPreferencesInput", + "description": "Autogenerated input type of UpdateNotificationPreferences", + "fields": null, + "inputFields": [ + { + "name": "accountId", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "postManually", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SetAssignmentPostPolicyPayload", - "description": "Autogenerated return type of SetAssignmentPostPolicy", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", + "kind": "ENUM", + "name": "NotificationPreferencesContextType", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "postPolicy", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "PostPolicy", - "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "SetCoursePostPolicyInput", - "description": "Autogenerated input type of SetCoursePostPolicy", - "fields": null, - "inputFields": [ - { - "name": "courseId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "courseId", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "postManually", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "enabled", + "description": null, + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SetCoursePostPolicyPayload", - "description": "Autogenerated return type of SetCoursePostPolicy", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "postPolicy", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "PostPolicy", - "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "SetFriendlyDescriptionInput", - "description": "Autogenerated input type of SetFriendlyDescription", - "fields": null, - "inputFields": [ - { - "name": "description", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "outcomeId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "hasReadPrivacyNotice", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "contextId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "sendObservedNamesInNotifications", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "contextType", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "sendScoresInEmails", + "description": null, + "type": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SetFriendlyDescriptionPayload", - "description": "Autogenerated return type of SetFriendlyDescription", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcomeFriendlyDescription", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "OutcomeFriendlyDescriptionType", - "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "SetModuleItemCompletionInput", - "description": "Autogenerated input type of SetModuleItemCompletion", - "fields": null, - "inputFields": [ - { - "name": "moduleId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "communicationChannelId", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "itemId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "frequency", + "description": null, + "type": { + "kind": "ENUM", + "name": "NotificationFrequencyType", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "done", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "isPolicyOverride", + "description": null, + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SetModuleItemCompletionPayload", - "description": "Autogenerated return type of SetModuleItemCompletion", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notificationCategory", + "description": null, + "type": { + "kind": "ENUM", + "name": "NotificationCategoryType", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateNotificationPreferencesPayload", + "description": "Autogenerated return type of UpdateNotificationPreferences.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "moduleItem", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [], + "type": { "kind": "OBJECT", - "name": "ModuleItem", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateOutcomeCalculationMethodInput", + "description": "Autogenerated input type of UpdateOutcomeCalculationMethod", + "fields": null, + "inputFields": [ + { + "name": "calculationInt", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", "ofType": null - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "SetOverrideScoreInput", - "description": "Autogenerated input type of SetOverrideScore", - "fields": null, - "inputFields": [ - { - "name": "enrollmentId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "calculationMethod", + "description": null, + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "gradingPeriodId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "overrideScore", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SetOverrideScorePayload", - "description": "Autogenerated return type of SetOverrideScore", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", + "kind": "SCALAR", + "name": "ID", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "grades", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Grades", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "String", - "description": "Represents textual data as UTF-8 character sequences. This type is most often used by GraphQL to represent free-form human-readable text.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "StudentSummaryAnalytics", - "description": "basic information about a students activity in a course", - "fields": [ - { - "name": "pageViews", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "PageViewAnalysis", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "participations", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "PageViewAnalysis", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "tardinessBreakdown", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "TardinessBreakdown", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SubHeader", - "description": null, - "fields": [ - { - "name": "modules", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateOutcomeCalculationMethodPayload", + "description": "Autogenerated return type of UpdateOutcomeCalculationMethod.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { - "kind": "OBJECT", - "name": "Module", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "title", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "ModuleItemInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Submission", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeCalculationMethod", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "OutcomeCalculationMethod", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assignment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attachment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "File", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attachments", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateOutcomeProficiencyInput", + "description": "Autogenerated input type of UpdateOutcomeProficiency", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "File", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attempt", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "body", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "commentsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "proficiencyRatings", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "OutcomeProficiencyRatingCreate", + "ofType": null + } + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateOutcomeProficiencyPayload", + "description": "Autogenerated return type of UpdateOutcomeProficiency.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "SubmissionCommentFilterInput", - "ofType": null - }, - "defaultValue": "{}" - } - ], - "type": { - "kind": "OBJECT", - "name": "SubmissionCommentConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deductedPoints", - "description": "how many points are being deducted due to late policy", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "enteredGrade", - "description": "the submission grade *before* late policy deductions were applied", - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "enteredScore", - "description": "the submission score *before* late policy deductions were applied", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "excused", - "description": "excused assignments are ignored when calculating grades", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "extraAttempts", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "feedbackForCurrentAttempt", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "grade", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradeHidden", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradeMatchesCurrentSubmission", - "description": "was the grade given on the current submission (resubmission)", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradingStatus", - "description": null, - "args": [], - "type": { - "kind": "ENUM", - "name": "SubmissionGradingStatus", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "outcomeProficiency", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "OutcomeProficiency", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "late", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "latePolicyStatus", - "description": null, - "args": [], - "type": { - "kind": "ENUM", - "name": "LatePolicyStatusType", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mediaObject", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "MediaObject", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "missing", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "posted", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "postedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "resourceLinkLookupUuid", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rubricAssessmentsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateRubricArchivedStateInput", + "description": "Autogenerated input type of UpdateRubricArchivedState", + "fields": null, + "inputFields": [ + { + "name": "archived", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateRubricArchivedStatePayload", + "description": "Autogenerated return type of UpdateRubricArchivedState.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rubric", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Rubric", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateRubricAssessmentReadStateInput", + "description": "Autogenerated input type of UpdateRubricAssessmentReadState", + "fields": null, + "inputFields": [ + { + "name": "submissionIds", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + } }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "SubmissionRubricAssessmentFilterInput", - "ofType": null - }, - "defaultValue": "{}" - } - ], - "type": { - "kind": "OBJECT", - "name": "RubricAssessmentConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "score", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "state", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "SubmissionState", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionDraft", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "SubmissionDraft", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionHistoriesConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateRubricAssessmentReadStatePayload", + "description": "Autogenerated return type of UpdateRubricAssessmentReadState.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissions", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Submission", + "ofType": null + } + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateSpeedGraderSettingsInput", + "description": "Autogenerated input type of UpdateSpeedGraderSettings", + "fields": null, + "inputFields": [ + { + "name": "gradeByQuestion", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "Boolean", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateSpeedGraderSettingsPayload", + "description": "Autogenerated return type of UpdateSpeedGraderSettings.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "SubmissionHistoryFilterInput", - "ofType": null - }, - "defaultValue": "{}" - } - ], - "type": { - "kind": "OBJECT", - "name": "SubmissionHistoryConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionStatus", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionType", - "description": null, - "args": [], - "type": { - "kind": "ENUM", - "name": "SubmissionType", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submittedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "turnitinData", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "speedGraderSettings", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", - "name": "TurnitinData", + "name": "SpeedGraderSettings", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unreadCommentCount", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "url", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "user", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "SubmissionInterface", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SubmissionComment", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "assignment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attachments", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateSplitScreenViewDeeplyNestedAlertInput", + "description": "Autogenerated input type of UpdateSplitScreenViewDeeplyNestedAlert", + "fields": null, + "inputFields": [ + { + "name": "splitScreenViewDeeplyNestedAlert", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "File", + "kind": "SCALAR", + "name": "Boolean", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attempt", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "author", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "comment", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "course", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Course", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mediaObject", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "MediaObject", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "read", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SubmissionCommentConnection", - "description": "The connection type for SubmissionComment.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "SubmissionCommentEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "SubmissionComment", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SubmissionCommentEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "SubmissionComment", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "SubmissionCommentFilterInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "allComments", - "description": "If all of the comments, regardless of the submission attempt, should be returned.\nIf this is true, the for_attempt argument will be ignored.\n", - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": "false" - }, - { - "name": "forAttempt", - "description": "What submission attempt the comments should be returned for. If not specified,\nit will return the comments for the current submisssion or submission history.\n", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": "null" - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SubmissionConnection", - "description": "The connection type for Submission.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "SubmissionEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Submission", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SubmissionDraft", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "activeSubmissionType", - "description": null, - "args": [], - "type": { - "kind": "ENUM", - "name": "DraftableSubmissionType", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attachments", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateSplitScreenViewDeeplyNestedAlertPayload", + "description": "Autogenerated return type of UpdateSplitScreenViewDeeplyNestedAlert.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", - "name": "File", + "name": "User", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "body", - "description": null, - "args": [ - { - "name": "rewriteUrls", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "externalTool", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "ExternalTool", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ltiLaunchUrl", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mediaObject", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "MediaObject", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "meetsAssignmentCriteria", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "meetsBasicLtiLaunchCriteria", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "meetsMediaRecordingCriteria", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "meetsStudentAnnotationCriteria", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "meetsTextEntryCriteria", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "meetsUploadCriteria", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "meetsUrlCriteria", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "resourceLinkLookupUuid", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionAttempt", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "url", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SubmissionEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "Submission", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "SubmissionFilterInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "states", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateSubmissionStickerInput", + "description": "Autogenerated input type of UpdateSubmissionSticker", + "fields": null, + "inputFields": [ + { + "name": "anonymousId", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "ENUM", - "name": "SubmissionState", + "kind": "SCALAR", + "name": "ID", "ofType": null } - } - }, - "defaultValue": "[submitted, pending_review, graded]" - }, - { - "name": "sectionIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assignmentId", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { @@ -25799,2785 +47617,871 @@ "name": "ID", "ofType": null } - } - }, - "defaultValue": null - }, - { - "name": "submittedSince", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "gradedSince", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "updatedSince", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "SubmissionGradingStatus", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "needs_grading", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "excused", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "needs_review", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "graded", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SubmissionHistory", - "description": null, - "fields": [ - { - "name": "assignment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attachment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "File", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attachments", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sticker", + "description": null, + "type": { + "kind": "ENUM", + "name": "Sticker", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateSubmissionStickerPayload", + "description": "Autogenerated return type of UpdateSubmissionSticker.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submission", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", - "name": "File", + "name": "Submission", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attempt", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "body", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "commentsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateSubmissionStudentEnteredScoreInput", + "description": "Autogenerated input type of UpdateSubmissionStudentEnteredScore", + "fields": null, + "inputFields": [ + { + "name": "enteredScore", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "Float", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "SubmissionCommentFilterInput", - "ofType": null - }, - "defaultValue": "{}" - } - ], - "type": { - "kind": "OBJECT", - "name": "SubmissionCommentConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deductedPoints", - "description": "how many points are being deducted due to late policy", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "enteredGrade", - "description": "the submission grade *before* late policy deductions were applied", - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "enteredScore", - "description": "the submission score *before* late policy deductions were applied", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "excused", - "description": "excused assignments are ignored when calculating grades", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "extraAttempts", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "feedbackForCurrentAttempt", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "grade", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradeHidden", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradeMatchesCurrentSubmission", - "description": "was the grade given on the current submission (resubmission)", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradingStatus", - "description": null, - "args": [], - "type": { - "kind": "ENUM", - "name": "SubmissionGradingStatus", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "late", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "latePolicyStatus", - "description": null, - "args": [], - "type": { - "kind": "ENUM", - "name": "LatePolicyStatusType", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mediaObject", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "MediaObject", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "missing", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "posted", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "postedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "resourceLinkLookupUuid", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rootId", - "description": "The canvas legacy id of the root submission this history belongs to\n", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rubricAssessmentsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateSubmissionStudentEnteredScorePayload", + "description": "Autogenerated return type of UpdateSubmissionStudentEnteredScore.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submission", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Submission", + "ofType": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateSubmissionsGradeInput", + "description": "Autogenerated input type of UpdateSubmissionsGrade", + "fields": null, + "inputFields": [ + { + "name": "score", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Int", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "ID", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "SubmissionRubricAssessmentFilterInput", - "ofType": null - }, - "defaultValue": "{}" - } - ], - "type": { - "kind": "OBJECT", - "name": "RubricAssessmentConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "score", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "state", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "SubmissionState", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionDraft", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "SubmissionDraft", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionStatus", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionType", - "description": null, - "args": [], - "type": { - "kind": "ENUM", - "name": "SubmissionType", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submittedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "turnitinData", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateSubmissionsGradePayload", + "description": "Autogenerated return type of UpdateSubmissionsGrade.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { - "kind": "OBJECT", - "name": "TurnitinData", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unreadCommentCount", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "parentAssignmentSubmission", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Submission", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submission", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Submission", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateSubmissionsGradeStatusInput", + "description": "Autogenerated input type of UpdateSubmissionsGradeStatus", + "fields": null, + "inputFields": [ + { + "name": "checkpointTag", + "description": null, + "type": { "kind": "SCALAR", - "name": "Int", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "url", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "user", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "SubmissionInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SubmissionHistoryConnection", - "description": "The connection type for SubmissionHistory.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "SubmissionHistoryEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "SubmissionHistory", + { + "name": "customGradeStatusId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SubmissionHistoryEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "SubmissionHistory", - "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "SubmissionHistoryFilterInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "states", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + { + "name": "latePolicyStatus", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionId", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "ENUM", - "name": "SubmissionState", + "kind": "SCALAR", + "name": "ID", "ofType": null } - } - }, - "defaultValue": "[deleted, graded, pending_review, submitted, ungraded, unsubmitted]" - }, - { - "name": "includeCurrentSubmission", - "description": "If the most current submission should be included in the submission\nhistory results. Defaults to true.\n", - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": "true" - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INTERFACE", - "name": "SubmissionInterface", - "description": "Types for submission or submission history", - "fields": [ - { - "name": "assignment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attachment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "File", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attachments", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateSubmissionsGradeStatusPayload", + "description": "Autogenerated return type of UpdateSubmissionsGradeStatus.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { - "kind": "OBJECT", - "name": "File", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "attempt", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "body", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "commentsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submission", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Submission", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateSubmissionsReadStateInput", + "description": "Autogenerated input type of UpdateSubmissionsReadState", + "fields": null, + "inputFields": [ + { + "name": "read", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "Boolean", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "SubmissionCommentFilterInput", - "ofType": null - }, - "defaultValue": "{}" - } - ], - "type": { - "kind": "OBJECT", - "name": "SubmissionCommentConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deductedPoints", - "description": "how many points are being deducted due to late policy", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "enteredGrade", - "description": "the submission grade *before* late policy deductions were applied", - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "enteredScore", - "description": "the submission score *before* late policy deductions were applied", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "excused", - "description": "excused assignments are ignored when calculating grades", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "extraAttempts", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "feedbackForCurrentAttempt", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "grade", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradeHidden", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradeMatchesCurrentSubmission", - "description": "was the grade given on the current submission (resubmission)", - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradingStatus", - "description": null, - "args": [], - "type": { - "kind": "ENUM", - "name": "SubmissionGradingStatus", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "late", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "latePolicyStatus", - "description": null, - "args": [], - "type": { - "kind": "ENUM", - "name": "LatePolicyStatusType", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mediaObject", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "MediaObject", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "missing", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "posted", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "postedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "resourceLinkLookupUuid", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "rubricAssessmentsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissionIds", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + } }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateSubmissionsReadStatePayload", + "description": "Autogenerated return type of UpdateSubmissionsReadState.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "submissions", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Submission", + "ofType": null + } + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateUserDiscussionsSplitscreenViewInput", + "description": "Autogenerated input type of UpdateUserDiscussionsSplitscreenView", + "fields": null, + "inputFields": [ + { + "name": "discussionsSplitscreenView", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "Boolean", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "filter", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "SubmissionRubricAssessmentFilterInput", - "ofType": null - }, - "defaultValue": "{}" - } - ], - "type": { - "kind": "OBJECT", - "name": "RubricAssessmentConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "score", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "state", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "SubmissionState", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionDraft", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "SubmissionDraft", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionStatus", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionType", - "description": null, - "args": [], - "type": { - "kind": "ENUM", - "name": "SubmissionType", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submittedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "turnitinData", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpdateUserDiscussionsSplitscreenViewPayload", + "description": "Autogenerated return type of UpdateUserDiscussionsSplitscreenView.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { - "kind": "OBJECT", - "name": "TurnitinData", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unreadCommentCount", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "url", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "user", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": [ - { - "kind": "OBJECT", - "name": "Submission", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "SubmissionHistory", - "ofType": null - } - ] - }, - { - "kind": "INPUT_OBJECT", - "name": "SubmissionOrderCriteria", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "field", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "SubmissionOrderField", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "direction", - "description": null, - "type": { - "kind": "ENUM", - "name": "OrderDirection", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "SubmissionOrderField", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "_id", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gradedAt", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "SubmissionRubricAssessmentFilterInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "forAttempt", - "description": "What submission attempt the rubric assessment should be returned for. If not\nspecified, it will return the rubric assessment for the current submisssion\nor submission history.\n", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": "null" - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "SubmissionSearchFilterInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "states", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "user", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "ENUM", - "name": "SubmissionState", + "kind": "OBJECT", + "name": "User", "ofType": null } - } - }, - "defaultValue": "[submitted, pending_review, graded]" - }, - { - "name": "sectionIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpsertCustomGradeStatusInput", + "description": "Autogenerated input type of UpsertCustomGradeStatus", + "fields": null, + "inputFields": [ + { + "name": "color", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null } - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "enrollmentTypes", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + { + "name": "id", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "ENUM", - "name": "EnrollmentType", + "kind": "SCALAR", + "name": "String", "ofType": null } - } - }, - "defaultValue": null - }, - { - "name": "userSearch", - "description": "The partial name or full ID of the users to match and return in the\nresults list. Must be at least 3 characters.\nQueries by administrative users will search on SIS ID, login ID, name, or email\naddress; non-administrative queries will only be compared against name.\n", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "scoredLessThan", - "description": "Limit results to submissions that scored below the specified value", - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "scoredMoreThan", - "description": "Limit results to submissions that scored above the specified value", - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "late", - "description": "Limit results to submissions that are late", - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "gradingStatus", - "description": "Limit results by grading status", - "type": { - "kind": "ENUM", - "name": "SubmissionGradingStatus", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "SubmissionSearchOrder", - "description": "Specify a sort for the results", - "fields": null, - "inputFields": [ - { - "name": "field", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "SubmissionSearchOrderField", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "direction", - "description": null, - "type": { - "kind": "ENUM", - "name": "OrderDirection", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "SubmissionSearchOrderField", - "description": "The user or submission field to sort by", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "username", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "score", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submitted_at", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "SubmissionState", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "submitted", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "unsubmitted", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pending_review", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "graded", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ungraded", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deleted", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "SubmissionType", - "description": "Types of submissions an assignment accepts", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "attendance", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "basic_lti_launch", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "discussion_topic", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "external_tool", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "media_recording", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "none", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "not_graded", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "on_paper", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "online_quiz", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "online_text_entry", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "online_upload", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "online_url", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "student_annotation", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "wiki_page", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "SubscribeToDiscussionTopicInput", - "description": "Autogenerated input type of SubscribeToDiscussionTopic", - "fields": null, - "inputFields": [ - { - "name": "discussionTopicId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "subscribed", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SubscribeToDiscussionTopicPayload", - "description": "Autogenerated return type of SubscribeToDiscussionTopic", - "fields": [ - { - "name": "discussionTopic", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpsertCustomGradeStatusPayload", + "description": "Autogenerated return type of UpsertCustomGradeStatus.", + "fields": [ + { + "name": "customGradeStatus", + "description": null, + "args": [], + "type": { "kind": "OBJECT", - "name": "Discussion", + "name": "CustomGradeStatus", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "UpsertStandardGradeStatusInput", + "description": "Autogenerated input type of UpsertStandardGradeStatus", + "fields": null, + "inputFields": [ + { + "name": "color", + "description": null, + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", + "kind": "SCALAR", + "name": "String", "ofType": null } - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "TardinessBreakdown", - "description": "statistics based on timeliness of student submissions", - "fields": [ - { - "name": "late", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "missing", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "onTime", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "total", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Term", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "id", + "description": null, + "type": { "kind": "SCALAR", "name": "ID", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "coursesConnection", - "description": "courses for this term", - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UpsertStandardGradeStatusPayload", + "description": "Autogenerated return type of UpsertStandardGradeStatus.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ValidationError", + "ofType": null + } + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CourseConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "endAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sisId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sisTermId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "startAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "description": "Contains timestamp metadata", - "fields": [ - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": [ - { - "kind": "OBJECT", - "name": "AccountDomain", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "AccountDomainLookup", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "AssessmentRequest", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "AssignmentGroup", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "AssignmentOverride", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "CommentBankItem", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "CommunicationChannel", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ContentTag", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Course", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Discussion", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "DiscussionEntry", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "DiscussionEntryDraft", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Enrollment", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ExternalTool", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ExternalUrl", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "File", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "GradingPeriod", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Group", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "GroupMembership", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "LearningOutcome", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Module", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ModuleExternalTool", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "ModuleItem", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Notification", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "NotificationPolicy", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "OutcomeFriendlyDescriptionType", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Page", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Progress", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Quiz", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Section", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Submission", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "SubmissionComment", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "SubmissionHistory", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "User", - "ofType": null - } - ] - }, - { - "kind": "UNION", - "name": "TurnitinContext", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": [ - { - "kind": "OBJECT", - "name": "File", - "ofType": null - }, - { - "kind": "OBJECT", - "name": "Submission", - "ofType": null - } - ] - }, - { - "kind": "OBJECT", - "name": "TurnitinData", - "description": null, - "fields": [ - { - "name": "score", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "status", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "target", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "UNION", - "name": "TurnitinContext", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "URL", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateAccountDomainLookupInput", - "description": "Autogenerated input type of UpdateAccountDomainLookup", - "fields": null, - "inputFields": [ - { - "name": "accountDomainId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "accountDomainLookupId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "standardGradeStatus", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "StandardGradeStatus", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "authenticationProvider", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "name", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateAccountDomainLookupPayload", - "description": "Autogenerated return type of UpdateAccountDomainLookup", - "fields": [ - { - "name": "accountDomainLookup", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "AccountDomainLookup", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UsageRights", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", + "kind": "SCALAR", + "name": "ID", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateAssignmentInput", - "description": "Autogenerated input type of UpdateAssignment", - "fields": null, - "inputFields": [ - { - "name": "state", - "description": null, - "type": { - "kind": "ENUM", - "name": "AssignmentState", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "dueAt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "lockAt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "unlockAt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "description", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "assignmentOverrides", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "AssignmentOverrideCreateOrUpdate", - "ofType": null - } - } - }, - "defaultValue": null - }, - { - "name": "position", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "pointsPossible", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "gradingType", - "description": null, - "type": { - "kind": "ENUM", - "name": "GradingType", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "allowedExtensions", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null } - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "assignmentGroupId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null + { + "name": "legalCopyright", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "groupSetId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null + { + "name": "license", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "allowedAttempts", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", + { + "name": "useJustification", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", "ofType": null }, - "defaultValue": null - }, - { - "name": "onlyVisibleToOverrides", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", + { + "kind": "INTERFACE", + "name": "Node", "ofType": null - }, - "defaultValue": null - }, - { - "name": "submissionTypes", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "SubmissionType", - "ofType": null - } - } - }, - "defaultValue": null - }, - { - "name": "peerReviews", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "AssignmentPeerReviewsUpdate", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "moderatedGrading", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "AssignmentModeratedGradingUpdate", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "gradeGroupStudentsIndividually", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "omitFromFinalGrade", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "anonymousInstructorAnnotations", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "postToSis", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "anonymousGrading", - "description": "requires anonymous_marking course feature to be set to true", - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "moduleIds", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "User", + "description": null, + "fields": [ + { + "name": "_id", + "description": "legacy canvas id", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { @@ -28585,291 +48489,647 @@ "name": "ID", "ofType": null } - } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "activityStream", + "description": null, + "args": [ + { + "name": "onlyActiveCourses", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ActivityStream", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "avatarUrl", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "URL", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "name", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateAssignmentPayload", - "description": "Autogenerated return type of UpdateAssignment", - "fields": [ - { - "name": "assignment", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Assignment", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "commentBankItemsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "query", + "description": "Only include comments that match the query string.\n", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "limit", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CommentBankItemConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "conversationsConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "scope", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ConversationParticipantConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseProgression", + "description": "Returns null if either of these conditions are met:\n* the course is not module based\n* no module in it has completion requirements\n* the queried user is not a student in the course\n* insufficient permissions for the request\n", + "args": [], + "type": { + "kind": "OBJECT", + "name": "CourseProgression", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseRoles", + "description": null, + "args": [ + { + "name": "builtInOnly", + "description": "Only return default/built_in roles", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "roleTypes", + "description": "Return only requested base role types", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } } - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateCommentBankItemInput", - "description": "Autogenerated input type of UpdateCommentBankItem", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "createdAt", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussionsSplitscreenView", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "comment", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "email", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateCommentBankItemPayload", - "description": "Autogenerated return type of UpdateCommentBankItem", - "fields": [ - { - "name": "commentBankItem", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "CommentBankItem", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enrollments", + "description": null, + "args": [ + { + "name": "courseId", + "description": "only return enrollments for this course", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "currentOnly", + "description": "Whether or not to restrict results to `active` enrollments in `available` courses", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "excludeConcluded", + "description": "Whether or not to exclude `completed` enrollments", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "horizonCourses", + "description": "Whether or not to include or exclude Canvas Career courses", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "orderBy", + "description": "The fields to order the results by", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Enrollment", + "ofType": null + } + } } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateConversationParticipantsInput", - "description": "Autogenerated input type of UpdateConversationParticipants", - "fields": null, - "inputFields": [ - { - "name": "conversationIds", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "favoriteCoursesConnection", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dashboardFilter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "DashboardObserveeFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CourseConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "favoriteGroupsConnection", + "description": "Favorite groups for the user.", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "includeNonCollaborative", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "GroupConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "groups", + "description": "**NOTE**: this only returns groups for the currently logged-in user.\n", + "args": [], + "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "ID", + "kind": "OBJECT", + "name": "Group", "ofType": null } } - } - }, - "defaultValue": null - }, - { - "name": "starred", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "subscribed", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "workflowState", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateConversationParticipantsPayload", - "description": "Autogenerated return type of UpdateConversationParticipants", - "fields": [ - { - "name": "conversationParticipants", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ConversationParticipant", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "htmlUrl", + "description": null, + "args": [ + { + "name": "courseId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + ], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", + "kind": "SCALAR", + "name": "ID", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateDiscussionEntriesReadStateInput", - "description": "Autogenerated input type of UpdateDiscussionEntriesReadState", - "fields": null, - "inputFields": [ - { - "name": "discussionEntryIds", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inboxLabels", + "description": null, + "args": [], + "type": { "kind": "LIST", "name": null, "ofType": { @@ -28877,2142 +49137,1384 @@ "name": null, "ofType": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null } } - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "read", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "instructureIdentityGlobalUserId", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateDiscussionEntriesReadStatePayload", - "description": "Autogenerated return type of UpdateDiscussionEntriesReadState", - "fields": [ - { - "name": "discussionEntries", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "DiscussionEntry", - "ofType": null - } - } + { + "name": "instructureIdentityOrganizationUserId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } + { + "name": "integrationId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateDiscussionEntryInput", - "description": "Autogenerated input type of UpdateDiscussionEntry", - "fields": null, - "inputFields": [ - { - "name": "discussionEntryId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "loginId", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "message", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "removeAttachment", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "fileId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "includeReplyPreview", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateDiscussionEntryParticipantInput", - "description": "Autogenerated input type of UpdateDiscussionEntryParticipant", - "fields": null, - "inputFields": [ - { - "name": "discussionEntryId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "read", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "rating", - "description": null, - "type": { - "kind": "ENUM", - "name": "RatingInputType", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "forcedReadState", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "reportType", - "description": null, - "type": { - "kind": "ENUM", - "name": "ReportType", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateDiscussionEntryParticipantPayload", - "description": "Autogenerated return type of UpdateDiscussionEntryParticipant", - "fields": [ - { - "name": "discussionEntry", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notificationPreferences", + "description": null, + "args": [], + "type": { "kind": "OBJECT", - "name": "DiscussionEntry", + "name": "NotificationPreferences", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notificationPreferencesEnabled", + "description": null, + "args": [ + { + "name": "accountId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextType", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "NotificationPreferencesContextType", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "courseId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateDiscussionEntryPayload", - "description": "Autogenerated return type of UpdateDiscussionEntry", - "fields": [ - { - "name": "discussionEntry", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "DiscussionEntry", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + ], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", + "kind": "SCALAR", + "name": "Boolean", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateDiscussionReadStateInput", - "description": "Autogenerated input type of UpdateDiscussionReadState", - "fields": null, - "inputFields": [ - { - "name": "discussionTopicId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "read", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "pronouns", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateDiscussionReadStatePayload", - "description": "Autogenerated return type of UpdateDiscussionReadState", - "fields": [ - { - "name": "discussionTopic", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "recipients", + "description": null, + "args": [ + { + "name": "context", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "search", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "OBJECT", - "name": "Discussion", + "name": "Recipients", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "recipientsObservers", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contextCode", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "recipientIds", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateDiscussionThreadReadStateInput", - "description": "Autogenerated input type of UpdateDiscussionThreadReadState", - "fields": null, - "inputFields": [ - { - "name": "discussionEntryId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", + ], + "type": { + "kind": "OBJECT", + "name": "MessageableUserConnection", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - }, - { - "name": "read", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "shortName", + "description": "A short name the user has selected, for use in conversations or other less formal places through the site.", + "args": [], + "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateDiscussionThreadReadStatePayload", - "description": "Autogenerated return type of UpdateDiscussionThreadReadState", - "fields": [ - { - "name": "discussionEntry", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "DiscussionEntry", + { + "name": "sisId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateDiscussionTopicInput", - "description": "Autogenerated input type of UpdateDiscussionTopic", - "fields": null, - "inputFields": [ - { - "name": "discussionTopicId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "sortableName", + "description": "The name of the user that is should be used for sorting groups of users, such as in the gradebook.", + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "published", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "locked", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateDiscussionTopicPayload", - "description": "Autogenerated return type of UpdateDiscussionTopic", - "fields": [ - { - "name": "discussionTopic", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "summaryAnalytics", + "description": null, + "args": [ + { + "name": "courseId", + "description": "returns summary analytics for this course", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "OBJECT", - "name": "Discussion", + "name": "StudentSummaryAnalytics", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalRecipients", + "description": null, + "args": [ + { + "name": "context", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateIsolatedViewDeeplyNestedAlertInput", - "description": "Autogenerated input type of UpdateIsolatedViewDeeplyNestedAlert", - "fields": null, - "inputFields": [ - { - "name": "isolatedViewDeeplyNestedAlert", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateIsolatedViewDeeplyNestedAlertPayload", - "description": "Autogenerated return type of UpdateIsolatedViewDeeplyNestedAlert", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + ], + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "ValidationError", + "kind": "SCALAR", + "name": "Int", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "user", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "User", - "ofType": null - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateLearningOutcomeGroupInput", - "description": "Autogenerated input type of UpdateLearningOutcomeGroup", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "DateTime", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "title", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "description", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "vendorGuid", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "parentOutcomeGroupId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateLearningOutcomeGroupPayload", - "description": "Autogenerated return type of UpdateLearningOutcomeGroup", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "learningOutcomeGroup", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "LearningOutcomeGroup", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateLearningOutcomeInput", - "description": "Autogenerated input type of UpdateLearningOutcome", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "uuid", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "title", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "displayName", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "description", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "vendorGuid", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "calculationMethod", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "calculationInt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "rubricCriterion", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "RubricCriterionInput", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateLearningOutcomePayload", - "description": "Autogenerated return type of UpdateLearningOutcome", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "viewableSubmissionsConnection", + "description": "All submissions with comments that the current_user is able to view", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filter", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "learningOutcome", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "LearningOutcome", + ], + "type": { + "kind": "OBJECT", + "name": "SubmissionConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "LegacyIDInterface", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateNotificationPreferencesInput", - "description": "Autogenerated input type of UpdateNotificationPreferences", - "fields": null, - "inputFields": [ - { - "name": "accountId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "courseId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "contextType", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "NotificationPreferencesContextType", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "enabled", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "hasReadPrivacyNotice", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "sendScoresInEmails", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "sendObservedNamesInNotifications", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "communicationChannelId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "notificationCategory", - "description": null, - "type": { - "kind": "ENUM", - "name": "NotificationCategoryType", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "frequency", - "description": null, - "type": { - "kind": "ENUM", - "name": "NotificationFrequencyType", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "isPolicyOverride", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateNotificationPreferencesPayload", - "description": "Autogenerated return type of UpdateNotificationPreferences", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "ValidationError", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "user", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "User", + { + "kind": "INTERFACE", + "name": "Node", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateOutcomeCalculationMethodInput", - "description": "Autogenerated input type of UpdateOutcomeCalculationMethod", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "calculationMethod", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "calculationInt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateOutcomeCalculationMethodPayload", - "description": "Autogenerated return type of UpdateOutcomeCalculationMethod", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + { + "kind": "INTERFACE", + "name": "Timestamped", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UserConnection", + "description": "The connection type for User.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { "kind": "OBJECT", - "name": "ValidationError", + "name": "UserEdge", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcomeCalculationMethod", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "OutcomeCalculationMethod", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UpdateOutcomeProficiencyInput", - "description": "Autogenerated input type of UpdateOutcomeProficiency", - "fields": null, - "inputFields": [ - { - "name": "id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "proficiencyRatings", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [], + "type": { + "kind": "LIST", "name": null, "ofType": { - "kind": "INPUT_OBJECT", - "name": "OutcomeProficiencyRatingCreate", + "kind": "OBJECT", + "name": "User", "ofType": null } - } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UpdateOutcomeProficiencyPayload", - "description": "Autogenerated return type of UpdateOutcomeProficiency", - "fields": [ - { - "name": "errors", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", - "name": "ValidationError", + "name": "PageInfo", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "outcomeProficiency", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "OutcomeProficiency", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "User", - "description": null, - "fields": [ - { - "name": "_id", - "description": "legacy canvas id", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "avatarUrl", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "URL", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "commentBankItemsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "UserEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null - }, - "defaultValue": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + } }, - { - "name": "query", - "description": "Only include comments that match the query string.\n", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null }, - { - "name": "limit", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CommentBankItemConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "conversationsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "ValidationError", + "description": null, + "fields": [ + { + "name": "attribute", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null - }, - "defaultValue": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } }, - { - "name": "scope", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null }, - { - "name": "filter", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "ConversationParticipantConnection", - "ofType": null + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "courseRoles", - "description": null, - "args": [ - { - "name": "courseId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null + { + "name": "isRepeatable", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null }, - { - "name": "roleTypes", - "description": "Return only requested base role types", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "String", + "kind": "ENUM", + "name": "__DirectiveLocation", "ofType": null } } - }, - "defaultValue": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } }, - { - "name": "builtInOnly", - "description": "Only return default/built_in roles", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onField", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + } + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + }, + { + "name": "onFragment", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "email", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "enrollments", - "description": null, - "args": [ - { - "name": "courseId", - "description": "only return enrollments for this course", - "type": { + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + }, + { + "name": "onOperation", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "ID", + "name": "Boolean", "ofType": null - }, - "defaultValue": null + } + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Location adjacent to a query operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Location adjacent to a mutation operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Location adjacent to a subscription operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Location adjacent to a field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Location adjacent to a fragment definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Location adjacent to a fragment spread.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Location adjacent to an inline fragment.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Location adjacent to a schema definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Location adjacent to a scalar definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Location adjacent to an object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Location adjacent to a field definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Location adjacent to an argument definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Location adjacent to an interface definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Location adjacent to a union definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Location adjacent to an enum definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Location adjacent to an enum value definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Location adjacent to an input object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Location adjacent to an input object field definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VARIABLE_DEFINITION", + "description": "Location adjacent to a variable definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null }, - { - "name": "currentOnly", - "description": "Whether or not to restrict results to `active` enrollments in `available` courses", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null - }, - "defaultValue": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } }, - { - "name": "orderBy", - "description": "The fields to order the results by", - "type": { + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "__InputValue", "ofType": null } } - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Enrollment", - "ofType": null - } } - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "favoriteCoursesConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "String", + "name": "Boolean", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "CourseConnection", - "ofType": null + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "defaultValue", + "description": "A GraphQL-formatted string representing the default value for this input value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "favoriteGroupsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "Boolean", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "String", "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "GroupConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "groups", - "description": "**NOTE**: this only returns groups for the currently logged-in user.\n", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", - "name": "Group", + "name": "__Type", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "description", + "description": null, + "args": [], + "type": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "integrationId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "notificationPreferences", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "NotificationPreferences", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "notificationPreferencesEnabled", - "description": null, - "args": [ - { - "name": "accountId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "courseId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null }, - { - "name": "contextType", - "description": null, - "type": { - "kind": "NON_NULL", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", "name": null, "ofType": { - "kind": "ENUM", - "name": "NotificationPreferencesContextType", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } } - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pronouns", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "recipients", - "description": null, - "args": [ - { - "name": "search", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + } }, - { - "name": "context", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "Recipients", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "shortName", - "description": "A short name the user has selected, for use in conversations or other less formal places through the site.", - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sisId", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sortableName", - "description": "The name of the user that is should be used for sorting groups of users, such as in the gradebook.", - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "submissionCommentsConnection", - "description": null, - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", "ofType": null - }, - "defaultValue": null + } }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "SubmissionCommentConnection", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "summaryAnalytics", - "description": null, - "args": [ - { - "name": "courseId", - "description": "returns summary analytics for this course", - "type": { - "kind": "NON_NULL", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", "name": null, "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } } - }, - "defaultValue": null - } - ], - "type": { - "kind": "OBJECT", - "name": "StudentSummaryAnalytics", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "DateTime", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - { - "kind": "INTERFACE", - "name": "Node", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "Timestamped", - "ofType": null - }, - { - "kind": "INTERFACE", - "name": "LegacyIDInterface", - "ofType": null - } - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UserConnection", - "description": "The connection type for User.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "UserEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "User", + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UserEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ValidationError", - "description": null, - "fields": [ - { - "name": "attribute", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "message", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "__Directive", - "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", - "fields": [ - { - "name": "args", - "description": null, - "args": [ - { - "name": "includeDeprecated", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": "false" - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "LIST", "name": null, "ofType": { @@ -31020,331 +50522,65 @@ "name": null, "ofType": { "kind": "OBJECT", - "name": "__InputValue", + "name": "__EnumValue", "ofType": null } } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "locations", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "ENUM", - "name": "__DirectiveLocation", + "kind": "OBJECT", + "name": "__Field", "ofType": null } } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "onField", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": true, - "deprecationReason": "Use `locations`." - }, - { - "name": "onFragment", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": true, - "deprecationReason": "Use `locations`." - }, - { - "name": "onOperation", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": true, - "deprecationReason": "Use `locations`." - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "__DirectiveLocation", - "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "QUERY", - "description": "Location adjacent to a query operation.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "MUTATION", - "description": "Location adjacent to a mutation operation.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "SUBSCRIPTION", - "description": "Location adjacent to a subscription operation.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "FIELD", - "description": "Location adjacent to a field.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "FRAGMENT_DEFINITION", - "description": "Location adjacent to a fragment definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "FRAGMENT_SPREAD", - "description": "Location adjacent to a fragment spread.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "INLINE_FRAGMENT", - "description": "Location adjacent to an inline fragment.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "SCHEMA", - "description": "Location adjacent to a schema definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "SCALAR", - "description": "Location adjacent to a scalar definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "OBJECT", - "description": "Location adjacent to an object type definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "FIELD_DEFINITION", - "description": "Location adjacent to a field definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ARGUMENT_DEFINITION", - "description": "Location adjacent to an argument definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "INTERFACE", - "description": "Location adjacent to an interface definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "UNION", - "description": "Location adjacent to a union definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ENUM", - "description": "Location adjacent to an enum definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ENUM_VALUE", - "description": "Location adjacent to an enum value definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "INPUT_OBJECT", - "description": "Location adjacent to an input object type definition.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "INPUT_FIELD_DEFINITION", - "description": "Location adjacent to an input object field definition.", - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "__EnumValue", - "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", - "fields": [ - { - "name": "deprecationReason", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "isDeprecated", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "__Field", - "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", - "fields": [ - { - "name": "args", - "description": null, - "args": [ - { - "name": "includeDeprecated", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": "false" - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { "kind": "LIST", "name": null, "ofType": { @@ -31356,197 +50592,15 @@ "ofType": null } } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deprecationReason", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "isDeprecated", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "type", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "__Type", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "__InputValue", - "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", - "fields": [ - { - "name": "defaultValue", - "description": "A GraphQL-formatted string representing the default value for this input value.", - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "deprecationReason", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "isDeprecated", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "type", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "__Type", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "__Schema", - "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", - "fields": [ - { - "name": "directives", - "description": "A list of all directives supported by this server.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { "kind": "LIST", "name": null, "ofType": { @@ -31554,63 +50608,75 @@ "name": null, "ofType": { "kind": "OBJECT", - "name": "__Directive", + "name": "__Type", "ofType": null } } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "mutationType", - "description": "If this server supports mutation, the type that mutation operations will be rooted at.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "__Type", - "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isOneOf", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "queryType", - "description": "The type that query operations will be rooted at.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { "kind": "OBJECT", "name": "__Type", "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "subscriptionType", - "description": "If this server support subscription, the type that subscription operations will be rooted at.", - "args": [], - "type": { - "kind": "OBJECT", - "name": "__Type", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "types", - "description": "A list of all types supported by this server.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { "kind": "LIST", "name": null, "ofType": { @@ -31622,346 +50688,211 @@ "ofType": null } } - } + }, + "isDeprecated": false, + "deprecationReason": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "__Type", - "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", - "fields": [ - { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "enumValues", - "description": null, - "args": [ - { - "name": "includeDeprecated", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": "false" - } - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + { + "name": "specifiedByURL", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given `__Type` is.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null, + "specifiedByURL": null, + "isOneOf": false + } + ], + "directives": [ + { + "name": "deprecated", + "description": "Marks an element of a GraphQL schema as no longer supported.", + "locations": [ + "FIELD_DEFINITION", + "ENUM_VALUE", + "ARGUMENT_DEFINITION", + "INPUT_FIELD_DEFINITION" + ], + "args": [ + { + "name": "reason", + "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"No longer supported\"", + "isDeprecated": false, + "deprecationReason": null + } + ], + "isRepeatable": false + }, + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "__EnumValue", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "fields", - "description": null, - "args": [ - { - "name": "includeDeprecated", - "description": null, - "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null - }, - "defaultValue": "false" - } - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "isRepeatable": false + }, + { + "name": "oneOf", + "description": "Requires that exactly one field must be supplied and that field must not be `null`.", + "locations": [ + "INPUT_OBJECT" + ], + "args": [], + "isRepeatable": false + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "__Field", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inputFields", - "description": null, - "args": [ - { - "name": "includeDeprecated", - "description": null, - "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null - }, - "defaultValue": "false" - } - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "__InputValue", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "interfaces", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "__Type", - "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "kind", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "__TypeKind", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ofType", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "__Type", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "possibleTypes", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "isRepeatable": false + }, + { + "name": "specifiedBy", + "description": "Exposes a URL that specifies the behavior of this scalar.", + "locations": [ + "SCALAR" + ], + "args": [ + { + "name": "url", + "description": "The URL that specifies the behavior of this scalar.", + "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "__Type", + "kind": "SCALAR", + "name": "String", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "__TypeKind", - "description": "An enum describing what kind of type a given `__Type` is.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "SCALAR", - "description": "Indicates this type is a scalar.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "OBJECT", - "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "INTERFACE", - "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "UNION", - "description": "Indicates this type is a union. `possibleTypes` is a valid field.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ENUM", - "description": "Indicates this type is an enum. `enumValues` is a valid field.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "INPUT_OBJECT", - "description": "Indicates this type is an input object. `inputFields` is a valid field.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "LIST", - "description": "Indicates this type is a list. `ofType` is a valid field.", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "NON_NULL", - "description": "Indicates this type is a non-null. `ofType` is a valid field.", - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - } - ], - "directives": [ - { - "name": "include", - "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", - "locations": [ - "FIELD", - "FRAGMENT_SPREAD", - "INLINE_FRAGMENT" - ], - "args": [ - { - "name": "if", - "description": "Included when true.", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "defaultValue": null - } - ] - }, - { - "name": "skip", - "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", - "locations": [ - "FIELD", - "FRAGMENT_SPREAD", - "INLINE_FRAGMENT" - ], - "args": [ - { - "name": "if", - "description": "Skipped when true.", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "defaultValue": null - } - ] - }, - { - "name": "deprecated", - "description": "Marks an element of a GraphQL schema as no longer supported.", - "locations": [ - "FIELD_DEFINITION", - "ENUM_VALUE", - "ARGUMENT_DEFINITION", - "INPUT_FIELD_DEFINITION" - ], - "args": [ - { - "name": "reason", - "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": "\"No longer supported\"" - } - ] - } - ] + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "isRepeatable": false + } + ], + "description": null + } } -} +} \ No newline at end of file diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/DiscussionTopicsApi.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/DiscussionTopicsApi.kt index 33846d98e7..d4f6e396bd 100644 --- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/DiscussionTopicsApi.kt +++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/DiscussionTopicsApi.kt @@ -25,6 +25,7 @@ import com.instructure.dataseeding.model.DiscussionTopicEntryRequest import com.instructure.dataseeding.model.DiscussionTopicEntryResponse import com.instructure.dataseeding.util.CanvasNetworkAdapter import com.instructure.dataseeding.util.Randomizer +import com.instructure.dataseedingapi.type.DiscussionTopicContextType import retrofit2.Call import retrofit2.http.Body import retrofit2.http.POST @@ -70,6 +71,50 @@ object DiscussionTopicsApi { .body()!! } + fun createDiscussionTopicWithCheckpoints(courseId: Long, token: String, discussionTitle: String, assignmentName: String) { + val apolloClient = CanvasNetworkAdapter.getApolloClient(token) + + val dates = listOf( + com.instructure.dataseedingapi.type.DiscussionCheckpointDate( + type = com.instructure.dataseedingapi.type.DiscussionCheckpointDateType.everyone, + ) + ) + + val checkpoints = listOf( + com.instructure.dataseedingapi.type.DiscussionCheckpoints( + checkpointLabel = com.instructure.dataseedingapi.type.CheckpointLabelType.reply_to_topic, + pointsPossible = 10.0, + dates = dates, + repliesRequired = com.apollographql.apollo.api.Optional.present(1) + ), + com.instructure.dataseedingapi.type.DiscussionCheckpoints( + checkpointLabel = com.instructure.dataseedingapi.type.CheckpointLabelType.reply_to_entry, + pointsPossible = 5.0, + dates = dates, + repliesRequired = com.apollographql.apollo.api.Optional.present(2) + ) + ) + + val assignment = com.instructure.dataseedingapi.type.AssignmentCreate( + name = assignmentName, + courseId = courseId.toString(), + gradingType = com.apollographql.apollo.api.Optional.present(com.instructure.dataseedingapi.type.GradingType.points), + forCheckpoints = com.apollographql.apollo.api.Optional.present(true), + ) + + val mutation = com.instructure.dataseedingapi.CreateDiscussionTopicMinimalMutation( + contextId = courseId.toString(), + contextType = DiscussionTopicContextType.Course, + title = discussionTitle, + assignment = assignment, + checkpoints = checkpoints + ) + + kotlinx.coroutines.runBlocking { + apolloClient.mutation(mutation).execute() + } + } + fun createAnnouncement(courseId: Long, token: String, lockedForUser: Boolean = false, locked: Boolean = false, announcementTitle: String? = null): DiscussionApiModel { val discussion = createDiscussion(courseId, token, true, lockedForUser, locked, announcementTitle) @@ -93,4 +138,9 @@ object DiscussionTopicsApi { locked = true ) } + + data class CreateDiscussionTopicGraphQLRequest( + val query: String, + val variables: Map + ) } diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/CanvasNetworkAdapter.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/CanvasNetworkAdapter.kt index e83efde3fd..b5bfe89173 100644 --- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/CanvasNetworkAdapter.kt +++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/CanvasNetworkAdapter.kt @@ -86,9 +86,26 @@ object CanvasNetworkAdapter { return ApolloClient.Builder() .serverUrl("https://mobileqa.beta.instructure.com/api/graphql/") .okHttpClient(okHttpClientForApollo(token)) + .addCustomScalarAdapter(com.instructure.dataseedingapi.type.DateTime.type, DateTimeAdapter()) .build() } + private class DateTimeAdapter : com.apollographql.apollo.api.Adapter { + override fun fromJson(reader: com.apollographql.apollo.api.json.JsonReader, customScalarAdapters: com.apollographql.apollo.api.CustomScalarAdapters): java.util.Date { + val dateString = reader.nextString() + return java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").apply { + timeZone = java.util.TimeZone.getTimeZone("UTC") + }.parse(dateString) ?: throw IllegalArgumentException("Invalid date: $dateString") + } + + override fun toJson(writer: com.apollographql.apollo.api.json.JsonWriter, customScalarAdapters: com.apollographql.apollo.api.CustomScalarAdapters, value: java.util.Date) { + val dateString = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").apply { + timeZone = java.util.TimeZone.getTimeZone("UTC") + }.format(value) + writer.value(dateString) + } + } + private val noAuthOkHttpClient: OkHttpClient by lazy { OkHttpClient.Builder() .retryOnConnectionFailure(true) diff --git a/automation/espresso/build.gradle b/automation/espresso/build.gradle index 77993530c0..4b7874ce96 100644 --- a/automation/espresso/build.gradle +++ b/automation/espresso/build.gradle @@ -37,7 +37,7 @@ allprojects { apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.devtools.ksp' apply plugin: 'dagger.hilt.android.plugin' android { @@ -91,6 +91,7 @@ dependencies { implementation project(':dataseedingapi') implementation project(':login-api-2') + implementation Libs.ANDROIDX_WORK_TEST androidTestImplementation Libs.COMPOSE_UI_TEST implementation project(':pandautils') @@ -174,9 +175,9 @@ dependencies { /* DI */ implementation Libs.HILT - kapt Libs.HILT_COMPILER + ksp Libs.HILT_COMPILER implementation Libs.HILT_TESTING - kapt Libs.HILT_TESTING_COMPILER + ksp Libs.HILT_TESTING_COMPILER implementation Libs.HILT_ANDROIDX_WORK implementation Libs.COMPOSE_UI_TEST_MANIFEST diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasRunner.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasRunner.kt index af88cb5b23..beb2a8e6c2 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasRunner.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasRunner.kt @@ -8,7 +8,7 @@ import android.view.accessibility.AccessibilityNodeInfo import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingResource import androidx.test.runner.AndroidJUnitRunner -import com.instructure.canvas.espresso.mockCanvas.MockCanvasInterceptor +import com.instructure.canvas.espresso.mockcanvas.MockCanvasInterceptor import com.instructure.canvasapi2.CanvasRestAdapter import com.jakewharton.espresso.OkHttp3IdlingResource diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt index c0125f35ec..4b76f23405 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt @@ -114,7 +114,8 @@ abstract class CanvasTest : InstructureTestingContract { } val application = originalActivity.application as? TestAppManager - application?.workerFactory = workerFactory + application?.workerFactory = this.workerFactory + application?.initWorkManager(application) } @Before @@ -137,24 +138,24 @@ abstract class CanvasTest : InstructureTestingContract { Log.d("TEST RETRY", "testMethod: $testMethod, error=$error, stacktrace=${error.stackTrace.joinToString("\n")} cause=${error.cause}") } - // Grab the Splunk-mobile token from Bitrise - val splunkToken = InstrumentationRegistry.getArguments().getString("SPLUNK_MOBILE_TOKEN") + // Grab the Observe-mobile token from Bitrise + val observeToken = InstrumentationRegistry.getArguments().getString("OBSERVE_MOBILE_TOKEN") // Only continue if we're on Bitrise // (More accurately, if we are on FTL launched from Bitrise.) - if(splunkToken != null && !splunkToken.isEmpty()) { - val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + if(!observeToken.isNullOrEmpty()) { + val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) val hasActiveNetwork = networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false if (hasActiveNetwork) { - reportToSplunk(disposition, testMethod, testClass, error, splunkToken) + reportToObserve(disposition, testMethod, testClass, error, observeToken) } else { turnOnConnectionViaADB() connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { super.onAvailable(network) connectivityManager.unregisterNetworkCallback(this) - reportToSplunk(disposition, testMethod, testClass, error, splunkToken) + reportToObserve(disposition, testMethod, testClass, error, observeToken) } }) } @@ -167,12 +168,12 @@ abstract class CanvasTest : InstructureTestingContract { } - private fun reportToSplunk( + private fun reportToObserve( disposition: String, testMethod: String, testClass: String, error: Throwable, - splunkToken: String? + observeToken: String? ) { val bitriseWorkflow = InstrumentationRegistry.getArguments().getString("BITRISE_TRIGGERED_WORKFLOW_ID") @@ -185,21 +186,24 @@ abstract class CanvasTest : InstructureTestingContract { eventObject.put("workflow", bitriseWorkflow) eventObject.put("branch", bitriseBranch) eventObject.put("bitriseApp", bitriseApp) + eventObject.put("buildNumber", bitriseBuildNumber) eventObject.put("status", disposition) eventObject.put("testName", testMethod) eventObject.put("testClass", testClass) eventObject.put("stackTrace", error.stackTrace.take(15).joinToString(", ")) eventObject.put("osVersion", Build.VERSION.SDK_INT.toString()) + eventObject.put("sourcetype", "mobile-android-qa-testresult") // Limit our error message to 4096 chars; they can be unreasonably long (e.g., 137K!) when // they contain a view hierarchy, and there is typically not much useful info after the // first few lines. eventObject.put("message", error.toString().take(4096)) val payloadObject = JSONObject() - payloadObject.put("sourcetype", "mobile-android-qa-testresult") - payloadObject.put("event", eventObject) + + payloadObject.put("data", eventObject) val payload = payloadObject.toString() + val payloadBytes = payload.toByteArray(Charsets.UTF_8) Log.d("CanvasTest", "payload = $payload") // Can't run a curl command from FTL, so let's do this the hard way @@ -210,11 +214,12 @@ abstract class CanvasTest : InstructureTestingContract { try { // Set up our url/connection - val url = URL("https://http-inputs-inst.splunkcloud.com:443/services/collector") + val url = URL("https://103443579803.collect.observeinc.com/v1/http") conn = url.openConnection() as HttpURLConnection conn.requestMethod = "POST" - conn.setRequestProperty("Authorization", "Splunk $splunkToken") + conn.setRequestProperty("Authorization", "Bearer $observeToken") conn.setRequestProperty("Content-Type", "application/json; utf-8") + conn.setRequestProperty("Content-Length", payloadBytes.size.toString()) conn.setRequestProperty("Accept", "application/json") conn.setDoInput(true) conn.setDoOutput(true) @@ -233,7 +238,7 @@ abstract class CanvasTest : InstructureTestingContract { "Response code: ${conn.responseCode}, message: ${conn.responseMessage}" ) - // Report the splunk result JSON + // Report the Observe result JSON inputStream = conn.inputStream val content = inputStream.bufferedReader().use(BufferedReader::readText) Log.d("CanvasTest", "Response: $content") @@ -271,7 +276,7 @@ abstract class CanvasTest : InstructureTestingContract { // Suppression (exclusion) rules ?.setSuppressingResultMatcher( anyOf( - // Extra supressions that can be adde to individual tests + // Extra suppressions that can be added to individual tests extraAccessibilitySupressions, // Suppress issues in PsPDFKit, date/time picker, calendar grid isExcludedNamedView( listOf("pspdf", "_picker_", "timePicker", "calendar1")), @@ -345,18 +350,18 @@ abstract class CanvasTest : InstructureTestingContract { // Weeding out id == -1 will filter out a lot of lines from our logs if(view.id != -1) { try { - var resourceName = view.context.resources.getResourceName(view.id) + val resourceName = view.context.resources.getResourceName(view.id) for (excludedName in excludes) { if (resourceName.contains(excludedName)) { //Log.v("AccessibilityExclude", "Caught $resourceName") return true } } - } catch (e: Resources.NotFoundException) { + } catch (_: Resources.NotFoundException) { } } - var parent = view.parent + val parent = view.parent when(parent) { is View -> view = parent else -> view = null @@ -392,7 +397,7 @@ abstract class CanvasTest : InstructureTestingContract { // in action bars being to narrow. fun withOnlyWidthLessThan(dimInDp: Int) : BaseMatcher { - var activity = activityRule.activity + val activity = activityRule.activity val densityDpi = activity.resources.displayMetrics.densityDpi val dim_f = dimInDp * (densityDpi.toDouble() / DisplayMetrics.DENSITY_DEFAULT.toDouble()) val dim = dim_f.toInt() @@ -431,7 +436,7 @@ abstract class CanvasTest : InstructureTestingContract { when(item) { is AccessibilityViewCheckResult -> { val contentDescription = item.view?.contentDescription - var result = (contentDescription?.contains("Overflow", ignoreCase = true) ?: false) || (contentDescription?.contains("More options", ignoreCase = true) ?: false) + val result = (contentDescription?.contains("Overflow", ignoreCase = true) ?: false) || (contentDescription?.contains("More options", ignoreCase = true) ?: false) //Log.v("overflowWidth", "isOverflowMenu: contentDescription=${item.view?.contentDescription ?: "unknown"}, result=$result ") return result } @@ -473,7 +478,7 @@ abstract class CanvasTest : InstructureTestingContract { (v.width < 48 && v.width < v.minimumWidth) ) if(toss) { - var resourceName = getResourceName(v) + val resourceName = getResourceName(v) Log.v("underMinSizeOnLowRes", "Tossing $resourceName, w=${v.width}, h=${v.height}, mw=${v.minimumWidth}, mh=${v.minimumHeight}") return true @@ -516,7 +521,7 @@ abstract class CanvasTest : InstructureTestingContract { inputStream.copyTo(outputStream) } finally { - if(inputStream != null) inputStream.close() + inputStream?.close() if(outputStream != null) { outputStream.flush() outputStream.close() diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomActions.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomActions.kt index fb87884d51..f6dcedf239 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomActions.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomActions.kt @@ -26,6 +26,7 @@ import android.widget.EditText import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.PerformException @@ -37,6 +38,7 @@ import androidx.test.espresso.action.GeneralClickAction import androidx.test.espresso.action.Press import androidx.test.espresso.action.Tap import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.RootMatchers.withDecorView @@ -354,4 +356,29 @@ fun checkToastText(@StringRes stringRes: Int, activity: Activity) { //Intentionally empty as we would like to wait for the toast to disappear. Somehow doesNotExist() doesn't work because it passes even if the toast is still there and visible. } } +} + +fun pressBackButton(times: Int) { + for(i in 1..times) { + Espresso.pressBack() + } +} + +fun waitForViewToDisappear(viewMatcher: Matcher, timeoutInSeconds: Long) { + val startTime = System.currentTimeMillis() + + while (System.currentTimeMillis() - startTime < (timeoutInSeconds * 1000)) { + try { + onView(viewMatcher) + .check(ViewAssertions.doesNotExist()) + return + } catch (e: AssertionError) { + Thread.sleep(200) + } + } + throw AssertionError("The view has not been displayed within $timeoutInSeconds seconds.") +} + +fun toString(view: View): String { + return HumanReadables.getViewHierarchyErrorMessage(view, null, "", null) } \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomMatchers.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomMatchers.kt index e8565f704f..f309b7b826 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomMatchers.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CustomMatchers.kt @@ -16,16 +16,23 @@ */ package com.instructure.canvas.espresso +import android.content.Intent +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.Drawable import android.util.DisplayMetrics import android.view.View import android.view.ViewGroup import android.widget.EditText +import android.widget.ImageView import android.widget.RadioButton import android.widget.TextView import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.Toolbar import androidx.constraintlayout.widget.ConstraintLayout import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.test.espresso.Espresso.onView import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction @@ -33,6 +40,7 @@ import androidx.test.espresso.ViewAssertion import androidx.test.espresso.ViewInteraction import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.BoundedMatcher +import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId @@ -42,8 +50,10 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.textfield.TextInputLayout import com.instructure.espresso.ActivityHelper +import com.instructure.pandautils.utils.ColorUtils import junit.framework.AssertionFailedError import org.hamcrest.BaseMatcher +import org.hamcrest.CoreMatchers.`is` import org.hamcrest.Description import org.hamcrest.Matcher import org.hamcrest.TypeSafeMatcher @@ -352,3 +362,189 @@ fun getText(matcher: Matcher): String { return text } +object SwipeRefreshLayoutMatchers { + fun isRefreshing(isRefreshing: Boolean): Matcher { + return object : BoundedMatcher(SwipeRefreshLayout::class.java) { + + override fun describeTo(description: Description) { + description.appendText(if (isRefreshing) "is refreshing" else "is not refreshing") + } + + override fun matchesSafely(view: SwipeRefreshLayout): Boolean { + return view.isRefreshing == isRefreshing + } + } + } +} + +object ViewSizeMatcher { + fun hasWidth(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { + override fun describeTo(description: Description) { + description.appendText("has a width of ${pixels}px") + } + + override fun matchesSafely(view: View): Boolean = view.width == pixels + } + + fun hasHeight(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { + override fun describeTo(description: Description) { + description.appendText("has a height of ${pixels}px") + } + + override fun matchesSafely(view: View): Boolean = view.height == pixels + } + + fun hasMinWidth(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { + override fun describeTo(description: Description) { + description.appendText("has a minimum width of ${pixels}px") + } + + override fun matchesSafely(view: View): Boolean = view.width >= pixels + } + + fun hasMinHeight(pixels: Int): Matcher = object : TypeSafeMatcher(View::class.java) { + override fun describeTo(description: Description) { + description.appendText("has a minimum height of ${pixels}px") + } + + override fun matchesSafely(view: View): Boolean = view.height >= pixels + } +} + +fun ViewInteraction.assertLineCount(lineCount: Int) { + val matcher = object : TypeSafeMatcher() { + override fun matchesSafely(item: View): Boolean { + return (item as TextView).lineCount == lineCount + } + + override fun describeTo(description: Description) { + description.appendText("isTextInLines") + } + } + check(matches(matcher)) +} + + +fun ViewInteraction.getView(): View { + lateinit var matchingView: View + perform(object : ViewAction { + override fun getDescription() = "Get View reference" + + override fun getConstraints(): Matcher { + return isAssignableFrom(View::class.java) + } + + override fun perform(uiController: UiController?, view: View) { + matchingView = view + } + }) + return matchingView +} + +fun ViewInteraction.assertCompletelyAbove(other: ViewInteraction) { + val view1 = getView() + val view2 = other.getView() + val location1 = view1.locationOnScreen + val location2 = view2.locationOnScreen + val isAbove = location1[1] + view1.height <= location2[1] + assertThat("completely above", isAbove, `is`(true)) +} + +fun ViewInteraction.assertCompletelyBelow(other: ViewInteraction) { + val view1 = getView() + val view2 = other.getView() + val location1 = view1.locationOnScreen + val location2 = view2.locationOnScreen + val isAbove = location2[1] + view2.height <= location1[1] + assertThat("completely below", isAbove, `is`(true)) +} + +val View.locationOnScreen get() = IntArray(2).apply { getLocationOnScreen(this) } + + +/** + * Asserts that the TextView uses the specified font size in scaled pixels + */ +fun ViewInteraction.assertFontSizeSP(expectedSP: Float) { + val matcher = object : TypeSafeMatcher(View::class.java) { + + override fun matchesSafely(target: View): Boolean { + if (target !is TextView) return false + val actualSP = target.textSize / target.getResources().displayMetrics.scaledDensity + return actualSP.compareTo(expectedSP) == 0 + } + + override fun describeTo(description: Description) { + description.appendText("with fontSize: ${expectedSP}px") + } + } + check(matches(matcher)) +} + +fun ViewInteraction.assertIsRefreshing(isRefreshing: Boolean) { + val matcher = object : BoundedMatcher(SwipeRefreshLayout::class.java) { + + override fun describeTo(description: Description) { + description.appendText(if (isRefreshing) "is refreshing" else "is not refreshing") + } + + override fun matchesSafely(view: SwipeRefreshLayout): Boolean { + return view.isRefreshing == isRefreshing + } + } + check(matches(matcher)) +} + +class IntentActionMatcher(private val intentType: String, private val dataMatcher: String) : TypeSafeMatcher() { + + override fun describeTo(description: Description?) { + description?.appendText("Intent Matcher") + } + + override fun matchesSafely(item: Intent?): Boolean { + return (intentType == item?.action) && (item?.dataString?.contains(dataMatcher) ?: false) + } +} + +// Adapted from https://medium.com/@dbottillo/android-ui-test-espresso-matcher-for-imageview-1a28c832626f +/** + * Matches ImageView (or ImageButton) with the drawable associated with [resourceId]. If [resourceId] < 0, will + * match against "no drawable" / "drawable is null". + * + * If the [color] param is non-null, then the drawable associated with [resourceId] will be colored + * prior to matching. + */ +class ImageViewDrawableMatcher(val resourceId: Int, val color: Int? = null) : TypeSafeMatcher( + ImageView::class.java) { + override fun describeTo(description: Description) { + description.appendText("with drawable from resource id: ") + description.appendValue(resourceId) + } + + override fun matchesSafely(target: View?): Boolean { + if (target !is ImageView) { + return false + } + val imageView = target + if (resourceId < 0) { + return imageView.drawable == null + } + val resources: Resources = target.getContext().getResources() + val expectedDrawable: Drawable = resources.getDrawable(resourceId) ?: return false + if(color != null) { + ColorUtils.colorIt(color, expectedDrawable) + } + val bitmap: Bitmap = getBitmap(imageView.getDrawable()) + val otherBitmap: Bitmap = getBitmap(expectedDrawable) + return bitmap.sameAs(otherBitmap) + } + + private fun getBitmap(drawable: Drawable): Bitmap { + val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, + drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()) + drawable.draw(canvas) + return bitmap + } +} diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt index 3674a850b8..255987aa60 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestAppManager.kt @@ -14,21 +14,51 @@ * limitations under the License. */package com.instructure.canvas.espresso +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import androidx.work.Configuration +import androidx.work.DefaultWorkerFactory import androidx.work.WorkerFactory +import androidx.work.testing.TestDriver +import androidx.work.testing.WorkManagerTestInitHelper import com.instructure.canvasapi2.AppManager import com.instructure.canvasapi2.utils.RemoteConfigUtils open class TestAppManager: AppManager() { + var testDriver: TestDriver? = null + + var workerFactory: WorkerFactory? = null + + @SuppressLint("RestrictedApi") override fun onCreate() { super.onCreate() RemoteConfigUtils.initialize() + + if (workerFactory == null) { + workerFactory = getWorkManagerFactory() + } } - var workerFactory: WorkerFactory? = null + @SuppressLint("RestrictedApi") override fun getWorkManagerFactory(): WorkerFactory { - return workerFactory ?: WorkerFactory.getDefaultWorkerFactory() + return workerFactory ?: DefaultWorkerFactory } override fun performLogoutOnAuthError() = Unit + + fun initWorkManager(context: Context) { + try { + val config = Configuration.Builder() + .setMinimumLoggingLevel(Log.DEBUG) + .setExecutor(java.util.concurrent.Executors.newSingleThreadExecutor()) + .setWorkerFactory(this.getWorkManagerFactory()) + .build() + WorkManagerTestInitHelper.initializeTestWorkManager(context, config) + testDriver = WorkManagerTestInitHelper.getTestDriver(context) + } catch (e: IllegalStateException) { + Log.w("TestAppManager", "WorkManager.initialize() failed, likely already initialized: ${e.message}") + } + } } diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestMetaData.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestMetaData.kt index 397bb72747..106fb425bc 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestMetaData.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/TestMetaData.kt @@ -44,7 +44,7 @@ enum class SecondaryFeatureCategory { GROUPS_DASHBOARD, GROUPS_FILES, GROUPS_ANNOUNCEMENTS, GROUPS_DISCUSSIONS, GROUPS_PAGES, GROUPS_PEOPLE, EVENTS_DISCUSSIONS, EVENTS_QUIZZES, EVENTS_ASSIGNMENTS, EVENTS_NOTIFICATIONS, SETTINGS_EMAIL_NOTIFICATIONS, MODULES_ASSIGNMENTS, MODULES_DISCUSSIONS, MODULES_FILES, MODULES_PAGES, MODULES_QUIZZES, OFFLINE_MODE, ALL_COURSES, CHANGE_USER, ASSIGNMENT_REMINDER, ASSIGNMENT_DETAILS, SMART_SEARCH, ADD_STUDENT, - SYLLABUS, SUMMARY, FRONT_PAGE, CANVAS_NETWORK, INBOX_SIGNATURE, ACCESS_TOKEN_EXPIRATION, SECTIONS + SYLLABUS, SUMMARY, FRONT_PAGE, CANVAS_NETWORK, INBOX_SIGNATURE, ACCESS_TOKEN_EXPIRATION, SECTIONS, HELP_MENU, DISCUSSION_CHECKPOINTS } enum class TestCategory { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/E2EAnnotation.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/E2EAnnotation.kt similarity index 80% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/E2EAnnotation.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/E2EAnnotation.kt index 02533bb941..98ada4bb68 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/E2EAnnotation.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/E2EAnnotation.kt @@ -1,4 +1,4 @@ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test will run "end-to-end", generating real network requests @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/FlakyE2EAnnotation.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/FlakyE2EAnnotation.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/FlakyE2EAnnotation.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/FlakyE2EAnnotation.kt index 7d2f3799dc..eab0993572 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/FlakyE2EAnnotation.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/FlakyE2EAnnotation.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test is stubbed out and not yet implemented only for landscape tests. @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/KnownBug.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/KnownBug.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/KnownBug.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/KnownBug.kt index d471ef40ea..1e3afc979b 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/KnownBug.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/KnownBug.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test is stubbed out and not yet implemented only for landscape tests. @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/OfflineE2E.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/OfflineE2E.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/OfflineE2E.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/OfflineE2E.kt index bd8864ec0c..96534c2841 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/OfflineE2E.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/OfflineE2E.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test is the part of the Offline mode test case suite. @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/ReleaseExclude.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/ReleaseExclude.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/ReleaseExclude.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/ReleaseExclude.kt index 382ce737a4..7beface496 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/ReleaseExclude.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/ReleaseExclude.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test is stubbed out from the release process because of 3rd party flakiness or any other reason (explained in the parameter). @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/RunsOnTablet.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/RunsOnTablet.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/RunsOnTablet.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/RunsOnTablet.kt index 95e546fb23..d671d6379f 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/RunsOnTablet.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/RunsOnTablet.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, it will surely run within the tablet FTL workflow @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubAnnotation.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubAnnotation.kt similarity index 81% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubAnnotation.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubAnnotation.kt index 2daf174572..debf2733e4 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubAnnotation.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubAnnotation.kt @@ -1,4 +1,4 @@ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test is stubbed out and not yet implemented @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubCoverageAnnotation.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubCoverageAnnotation.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubCoverageAnnotation.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubCoverageAnnotation.kt index 66d7c43636..e329c0fca4 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubCoverageAnnotation.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubCoverageAnnotation.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test is stubbed out from the coverage workflow. @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubLandscapeAnnotation.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubLandscapeAnnotation.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubLandscapeAnnotation.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubLandscapeAnnotation.kt index a72f017302..1e5c97f7b1 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubLandscapeAnnotation.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubLandscapeAnnotation.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // When applied to a test method, denotes that the test is stubbed out and not yet implemented only for landscape tests. @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubMultiAPILevel.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubMultiAPILevel.kt index 9a4b28306c..1a20bec7a2 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubMultiAPILevel.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations // Apply on a test method which is failing on Firebase Test Lab (FTL) on some API levels. Write the failing API Levels into the parameter. @Target(AnnotationTarget.FUNCTION) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubTabletAnnotation.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubTabletAnnotation.kt similarity index 93% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubTabletAnnotation.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubTabletAnnotation.kt index a4b73da2b9..c2c632c993 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubTabletAnnotation.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/annotations/StubTabletAnnotation.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso +package com.instructure.canvas.espresso.annotations @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt index 8c431f4240..86f412c8b8 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt @@ -16,19 +16,18 @@ package com.instructure.canvas.espresso.common.interaction import com.instructure.canvas.espresso.CanvasComposeTest -import com.instructure.canvas.espresso.StubLandscape -import com.instructure.canvas.espresso.StubTablet +import com.instructure.canvas.espresso.annotations.StubLandscape import com.instructure.canvas.espresso.common.pages.compose.CalendarEventDetailsPage import com.instructure.canvas.espresso.common.pages.compose.CalendarFilterPage import com.instructure.canvas.espresso.common.pages.compose.CalendarScreenPage import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoDetailsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToAssignment -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addPlannable -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addCourseCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToAssignment +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addPlannable +import com.instructure.canvas.espresso.mockcanvas.addQuizToCourse import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.PlannableType import com.instructure.canvasapi2.models.Quiz diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt index 4f0617765f..6879db01d8 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt @@ -21,8 +21,8 @@ import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.compose.CalendarEventCreateEditPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventDetailsPage import com.instructure.canvas.espresso.common.pages.compose.CalendarScreenPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addUserCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addUserCalendarEvent import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.DateHelper diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt index 9946c1ecce..1408ff5037 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt @@ -21,8 +21,8 @@ import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.compose.CalendarScreenPage import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoCreateUpdatePage import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoDetailsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addPlannable +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addPlannable import com.instructure.canvasapi2.models.PlannableType import com.instructure.canvasapi2.models.User import org.junit.Test diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/EventDetailsInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/EventDetailsInteractionTest.kt index 398966cb6d..04fd8546d9 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/EventDetailsInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/EventDetailsInteractionTest.kt @@ -21,8 +21,8 @@ import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.compose.CalendarEventCreateEditPage import com.instructure.canvas.espresso.common.pages.compose.CalendarEventDetailsPage import com.instructure.canvas.espresso.common.pages.compose.CalendarScreenPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCourseCalendarEvent +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addCourseCalendarEvent import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.utils.DateHelper import com.instructure.canvasapi2.utils.toApiString diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt index cd6efb603d..9580c803ff 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/GradesInteractionTest.kt @@ -18,12 +18,12 @@ package com.instructure.canvas.espresso.common.interaction import com.instructure.canvas.espresso.CanvasComposeTest -import com.instructure.canvas.espresso.StubLandscape -import com.instructure.canvas.espresso.StubTablet +import com.instructure.canvas.espresso.annotations.StubLandscape +import com.instructure.canvas.espresso.annotations.StubTablet import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage import com.instructure.canvas.espresso.common.pages.compose.GradesPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addGradingPeriod +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addGradingPeriod import com.instructure.canvasapi2.models.GradingPeriod import com.instructure.canvasapi2.utils.NumberHelper import com.instructure.espresso.ModuleItemInteractions @@ -34,7 +34,7 @@ import org.junit.Test abstract class GradesInteractionTest : CanvasComposeTest() { private val gradesPage = GradesPage(composeTestRule) - private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) + private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(), composeTestRule) @Test fun groupHeaderCollapsesAndExpandsOnClick() { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxComposeInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxComposeInteractionTest.kt index b1aa69f95c..257f2f802d 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxComposeInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxComposeInteractionTest.kt @@ -5,8 +5,8 @@ import com.instructure.canvas.espresso.common.pages.InboxPage import com.instructure.canvas.espresso.common.pages.compose.InboxComposePage import com.instructure.canvas.espresso.common.pages.compose.RecipientPickerPage import com.instructure.canvas.espresso.common.pages.compose.SelectContextPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addSentConversation +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addSentConversation import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.Course diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxDetailsInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxDetailsInteractionTest.kt index 654ac4a995..dccf2f00f8 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxDetailsInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxDetailsInteractionTest.kt @@ -19,8 +19,8 @@ import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.InboxPage import com.instructure.canvas.espresso.common.pages.compose.InboxComposePage import com.instructure.canvas.espresso.common.pages.compose.InboxDetailsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.utils.Randomizer +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.utils.Randomizer import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.User import org.junit.Test diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxListInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxListInteractionTest.kt index 2d5709303f..806f1af8c7 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxListInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxListInteractionTest.kt @@ -22,11 +22,11 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.common.pages.InboxPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addConversation -import com.instructure.canvas.espresso.mockCanvas.addConversations -import com.instructure.canvas.espresso.mockCanvas.addConversationsToCourseMap -import com.instructure.canvas.espresso.mockCanvas.createBasicConversation +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addConversation +import com.instructure.canvas.espresso.mockcanvas.addConversations +import com.instructure.canvas.espresso.mockcanvas.addConversationsToCourseMap +import com.instructure.canvas.espresso.mockcanvas.createBasicConversation import com.instructure.canvas.espresso.refresh import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.User diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxSignatureInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxSignatureInteractionTest.kt index 83e2e5cde5..f170ca9f1d 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxSignatureInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxSignatureInteractionTest.kt @@ -18,7 +18,7 @@ package com.instructure.canvas.espresso.common.interaction import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.compose.InboxSignatureSettingsPage import com.instructure.canvas.espresso.common.pages.compose.SettingsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import org.junit.Test abstract class InboxSignatureInteractionTest : CanvasComposeTest() { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SettingsInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SettingsInteractionTest.kt index 24fb85ea8d..97d7214018 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SettingsInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SettingsInteractionTest.kt @@ -16,7 +16,7 @@ import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.compose.SettingsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import org.junit.Test abstract class SettingsInteractionTest : CanvasComposeTest() { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt index 6c801164a8..0f11c28ac1 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SmartSearchInteractionTest.kt @@ -19,10 +19,10 @@ import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage import com.instructure.canvas.espresso.common.pages.compose.SmartSearchPage import com.instructure.canvas.espresso.common.pages.compose.SmartSearchPreferencesPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addPageToCourse +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAssignment +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addPageToCourse import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Page import com.instructure.canvasapi2.models.SmartSearchContentType @@ -35,7 +35,7 @@ abstract class SmartSearchInteractionTest : CanvasComposeTest() { private val smartSearchPage = SmartSearchPage(composeTestRule) private val smartSearchPreferencesPage = SmartSearchPreferencesPage(composeTestRule) - private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) + private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(), composeTestRule) @Test fun assertQuery() { @@ -226,6 +226,71 @@ abstract class SmartSearchInteractionTest : CanvasComposeTest() { smartSearchPage.assertItemDisplayed("Test Assignment for query", "Assignment") } + @Test + fun cancelFiltersDoesNotChangeFilterLogic() { + val data = initData() + + val course = data.courses.values.first() + val pageId = Random.nextLong(0, 100) + data.addPageToCourse( + course.id, + pageId, + title = "Test Page for query", + body = "Test Body for query", + url = "https://mock-data.instructure.com/courses/${course.id}/pages/$pageId" + ) + + data.addDiscussionTopicToCourse( + course, + data.teachers.first(), + topicTitle = "Test Discussion for query", + isAnnouncement = false + ) + + data.addDiscussionTopicToCourse( + course, + data.teachers.first(), + topicTitle = "Test Announcement for query", + isAnnouncement = true + ) + + data.addAssignment(course.id, name = "Test Assignment for query") + + goToSmartSearch(data, "query") + + composeTestRule.waitForIdle() + + smartSearchPage.assertItemDisplayed("Test Page for query", "Page") + smartSearchPage.assertItemDisplayed("Test Discussion for query", "Discussion") + smartSearchPage.assertItemDisplayed("Test Announcement for query", "Announcement") + smartSearchPage.assertItemDisplayed("Test Assignment for query", "Assignment") + + smartSearchPage.clickOnFilters() + + smartSearchPreferencesPage.clickOnFilter(SmartSearchFilter.PAGES) + smartSearchPreferencesPage.selectTypeSortType() + + smartSearchPreferencesPage.cancelFilters() + + smartSearchPage.assertGroupHeaderNotDisplayed(SmartSearchContentType.WIKI_PAGE) + smartSearchPage.assertGroupHeaderNotDisplayed(SmartSearchContentType.DISCUSSION_TOPIC) + smartSearchPage.assertGroupHeaderNotDisplayed(SmartSearchContentType.ANNOUNCEMENT) + smartSearchPage.assertGroupHeaderNotDisplayed(SmartSearchContentType.ASSIGNMENT) + + smartSearchPage.assertItemDisplayed("Test Page for query", "Page") + smartSearchPage.assertItemDisplayed("Test Discussion for query", "Discussion") + smartSearchPage.assertItemDisplayed("Test Announcement for query", "Announcement") + smartSearchPage.assertItemDisplayed("Test Assignment for query", "Assignment") + + smartSearchPage.clickOnFilters() + + smartSearchPreferencesPage.assertRadioButtonSelected("Relevance") + smartSearchPreferencesPage.assertFilterChecked(SmartSearchFilter.PAGES) + smartSearchPreferencesPage.assertFilterChecked(SmartSearchFilter.DISCUSSION_TOPICS) + smartSearchPreferencesPage.assertFilterChecked(SmartSearchFilter.ANNOUNCEMENTS) + smartSearchPreferencesPage.assertFilterChecked(SmartSearchFilter.ASSIGNMENTS) + } + @Test fun assertGroups() { val data = initData() diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/ToDoDetailsInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/ToDoDetailsInteractionTest.kt index 92a94d0deb..3dbaf29704 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/ToDoDetailsInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/ToDoDetailsInteractionTest.kt @@ -22,8 +22,8 @@ import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.common.pages.compose.CalendarScreenPage import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoCreateUpdatePage import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoDetailsPage -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addPlannable +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addPlannable import com.instructure.canvasapi2.models.PlannableType import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.toDate diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt index 5aa1f845fe..6ae2222815 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt @@ -18,6 +18,13 @@ package com.instructure.canvas.espresso.common.pages import android.view.View import android.widget.ScrollView +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag import androidx.test.espresso.AmbiguousViewMatcherException import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onData @@ -68,10 +75,9 @@ import org.hamcrest.Matchers.anyOf import org.hamcrest.Matchers.anything import org.hamcrest.Matchers.not -open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteractions) : BasePage(R.id.assignmentDetailsPage) { +open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteractions, val composeTestRule: ComposeTestRule) : BasePage(R.id.assignmentDetailsPage) { val toolbar by OnViewWithId(R.id.toolbar) val points by OnViewWithId(R.id.points) - val date by OnViewWithId(R.id.dueDateTextView) val submissionTypes by OnViewWithId(R.id.submissionTypesTextView) fun assertDisplayToolbarTitle() { @@ -83,11 +89,11 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti } fun assertDisplayToolbarSubtitle(courseNameText: String) { - onView(allOf(withText(courseNameText), withParent(R.id.toolbar))).assertDisplayed() + onView(allOf(withText(courseNameText), withParent(R.id.toolbar), withAncestor(R.id.assignmentDetailsPage))).assertDisplayed() } - fun assertDisplaysDate(dateText: String) { - date.assertHasText(dateText) + fun assertDisplaysDate(dateText: String, position: Int = 0) { + composeTestRule.onNodeWithTag("dueDateText-$position").assertTextEquals(dateText).isDisplayed() } fun assertAssignmentDetails(assignment: Assignment) { @@ -176,6 +182,10 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti assertStatus(R.string.submitted) } + fun assertStatusLate() { + assertStatus(R.string.lateSubmissionLabel) + } + fun assertStatusNotSubmitted() { assertStatus(R.string.notSubmitted) } @@ -204,7 +214,7 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti fun openOverflowMenu() { Espresso.onView( allOf( - ViewMatchers.withContentDescription(stringContainsTextCaseInsensitive("More options")), + withContentDescription(stringContainsTextCaseInsensitive("More options")), isDisplayed() )).click() } @@ -250,8 +260,23 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti onView(anyOf(withText(submissionType) + withAncestor(R.id.customPanel), withId(R.id.submissionTypesTextView) + withText(submissionType))).assertDisplayed() } - fun assertReminderViewDisplayed() { - onView(withId(R.id.reminderComposeView)).assertDisplayed() + fun assertReminderViewDisplayed(position: Int = 0) { + composeTestRule.onNodeWithTag("reminderView-$position").assertExists() + } + + fun assertCheckpointDisplayed(position: Int, name: String, grade: String) { + composeTestRule.onNode( + hasTestTag("checkpointName") + .and(hasText(name)) + .and(hasAnyAncestor(hasTestTag("checkpointItem-$position"))), + useUnmergedTree = true + ).assertExists() + composeTestRule.onNode( + hasTestTag("checkpointGrade") + .and(hasText(grade)) + .and(hasAnyAncestor(hasTestTag("checkpointItem-$position"))), + useUnmergedTree = true + ).assertExists() } fun assertNoDescriptionViewDisplayed() { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/ReminderPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentReminderPage.kt similarity index 98% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/ReminderPage.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentReminderPage.kt index 9523ceba2d..9867a17115 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/ReminderPage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentReminderPage.kt @@ -53,7 +53,7 @@ import org.hamcrest.Matchers.anything import java.util.Calendar -class ReminderPage(private val composeTestRule: ComposeTestRule) { +class AssignmentReminderPage(private val composeTestRule: ComposeTestRule) { private val reminderTitle = "Reminder" private val reminderDescription = "Add due date reminder notifications about this assignment on this device." private val reminderAdd = "Add reminder" diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/WrongDomainPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/WrongDomainPage.kt new file mode 100644 index 0000000000..f99460e80d --- /dev/null +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/WrongDomainPage.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.canvas.espresso.common.pages + +import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches +import androidx.test.espresso.web.sugar.Web.onWebView +import androidx.test.espresso.web.webdriver.DriverAtoms.findElement +import androidx.test.espresso.web.webdriver.DriverAtoms.getText +import androidx.test.espresso.web.webdriver.Locator +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.page.BasePage +import com.instructure.loginapi.login.R +import org.hamcrest.CoreMatchers.containsString + +/** + * Represents the Wrong Domain error page. + * + * This page provides functionality for interacting with the Canvas 404 error page that appears + * in a WebView when a user enters a domain that doesn't exist or is not authorized. + * It extends the BasePage class and contains methods to assert the error message and navigate back. + */ +@Suppress("unused") +class WrongDomainPage : BasePage() { + + private val signInRoot by OnViewWithId(R.id.signInRoot, autoAssert = false) + private val toolbar by OnViewWithId(R.id.toolbar, autoAssert = false) + + /** + * Asserts that the page objects are displayed (toolbar and webview container). + */ + override fun assertPageObjects(duration: Long) { + signInRoot.assertDisplayed() + toolbar.assertDisplayed() + } + + /** + * Asserts that the "You typed:" message is displayed, confirming the error page loaded. + * This text is immediately visible at the top of the error page. + * @param domain The domain that was typed (e.g., "wrong-domain"). + */ + fun assertYouTypedMessageDisplayed(domain: String) { + onWebView() + .withElement(findElement(Locator.TAG_NAME, "body")) + .check(webMatches(getText(), containsString("You typed: $domain.instructure.com"))) + } + + /** + * Asserts that the Canvas 404 error page contains an image element. + * This verifies the Canvas dinosaur (Instructure-saurus) error image is present. + */ + fun assertErrorPageImageDisplayed() { + onWebView().withElement(findElement(Locator.TAG_NAME, "img")) + } + +} \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/AssignmentListPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/AssignmentListPage.kt index b68812814a..f53779a31b 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/AssignmentListPage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/AssignmentListPage.kt @@ -16,6 +16,7 @@ */ package com.instructure.canvas.espresso.common.pages.compose +import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.hasAnyDescendant @@ -71,6 +72,16 @@ class AssignmentListPage(private val composeTestRule: ComposeTestRule) { composeTestRule.waitForIdle() } + fun clickAssignment(assignmentName: String) { + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("assignmentList") + .performScrollToNode(hasText(assignmentName)) + + composeTestRule.onNodeWithText(assignmentName) + .performClick() + composeTestRule.waitForIdle() + } + fun clickQuiz(quiz: QuizApiModel) { composeTestRule.onNodeWithTag("assignmentList") .performScrollToNode(hasText(quiz.title)) @@ -101,6 +112,10 @@ class AssignmentListPage(private val composeTestRule: ComposeTestRule) { assertHasAssignmentCommon(assignment.name, assignment.dueAt, needsGradingLabel) } + fun assertHasAssignmentWithCheckpoints(assignmentName: String, dueAtString: String = "No due date", expectedGrade: String? = null) { + assertHasAssignmentCommon(assignmentName, dueAtString, expectedGrade, true) + } + fun assertAssignmentNotDisplayed(assignmentName: String) { onView(withText(assignmentName) + withId(R.id.title) + hasSibling(withId(R.id.description))).check( doesNotExist() @@ -127,24 +142,59 @@ class AssignmentListPage(private val composeTestRule: ComposeTestRule) { .performClick() } - private fun assertHasAssignmentCommon(assignmentName: String, assignmentDueAt: String?, expectedLabel: String? = null) { + fun clickDiscussionCheckpointExpandCollapseIcon(discussionTitle: String) { + composeTestRule.onNode(hasTestTag("expandDiscussionCheckpoints") and hasParent(hasAnyDescendant(hasText(discussionTitle))), useUnmergedTree = true) + .performClick() + composeTestRule.waitForIdle() + } + + fun assertDiscussionCheckpointDetails(additionalRepliesCount: Int, dueAtReplyToTopic: String, gradeReplyToTopic: String, dueAtAdditionalReplies: String = dueAtReplyToTopic, gradeAdditionalReplies: String = gradeReplyToTopic) { + //'Reply to topic' checkpoint + composeTestRule.onNode(hasTestTag("checkpointName") and hasText("Reply to topic"), useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNode(hasTestTag("checkpointDueDate_Reply to topic") and hasText(dueAtReplyToTopic), true).assertIsDisplayed() + composeTestRule.onNode( hasTestTag("checkpointGradeText") and hasText(gradeReplyToTopic), useUnmergedTree = true).assertIsDisplayed() - // Check that either the assignment due date is present, or "No Due Date" is displayed - if(assignmentDueAt != null) { - composeTestRule.onNode( - hasText(assignmentName).and( - hasParent(hasAnyDescendant(hasText("Due ${assignmentDueAt.toDate()!!.toFormattedString()}"))) - ) - ) - .assertIsDisplayed() + //'Additional replies' checkpoint + composeTestRule.onNode(hasTestTag("checkpointName") and hasText("Additional replies ($additionalRepliesCount)"), useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNode(hasTestTag("checkpointDueDate_Additional replies ($additionalRepliesCount)") and hasText(dueAtAdditionalReplies), true).assertIsDisplayed() + composeTestRule.onNode( hasTestTag("checkpointGradeText") and hasText(gradeAdditionalReplies), useUnmergedTree = true).assertIsDisplayed() + } + + private fun assertHasAssignmentCommon(assignmentName: String, assignmentDueAt: String?, expectedLabel: String? = null, hasCheckPoints : Boolean = false) { + + // Check if the assignment is a discussion with checkpoints, if yes, we are expecting 2 due dates for the 2 checkpoints. + if(hasCheckPoints) { + composeTestRule.onAllNodes( + hasText("No due date").and( + hasParent(hasAnyDescendant(hasText(assignmentName))) + ), + true).assertCountEquals(2) } else { - composeTestRule.onNode( - hasText(assignmentName).and( - hasParent(hasAnyDescendant(hasText("No due date"))) + // Check that either the assignment due date is present, or "No Due Date" is displayed + if (assignmentDueAt != null) { + composeTestRule.onNode( + hasText(assignmentName).and( + hasParent( + hasAnyDescendant( + hasText( + "Due ${ + assignmentDueAt.toDate()!!.toFormattedString() + }" + ) + ) + ) + ) ) - ) - .assertIsDisplayed() + .assertIsDisplayed() + } else { + composeTestRule.onNode( + hasText(assignmentName).and( + hasParent(hasAnyDescendant(hasText("No due date"))) + ) + ) + .assertIsDisplayed() + } } retryWithIncreasingDelay(times = 10, maxDelay = 4000, catchBlock = { refresh() }) { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/GradesPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/GradesPage.kt index 550feb2e45..331db6eb0e 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/GradesPage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/GradesPage.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.test.performScrollToNode import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown import androidx.compose.ui.test.swipeUp -import com.instructure.composeTest.hasDrawable +import com.instructure.composetest.hasDrawable import com.instructure.pandautils.R diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SettingsPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SettingsPage.kt index fa84278cc2..fec009cb42 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SettingsPage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SettingsPage.kt @@ -37,6 +37,7 @@ import com.instructure.espresso.page.onView import com.instructure.espresso.page.onViewWithText import com.instructure.espresso.page.withText import com.instructure.espresso.retry +import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.pandautils.utils.AppTheme class SettingsPage(private val composeTestRule: ComposeTestRule) : BasePage() { @@ -71,7 +72,7 @@ class SettingsPage(private val composeTestRule: ComposeTestRule) : BasePage() { fun clickOnSettingsItem(title: String) { val nodeMatcher = hasTestTag("settingsItem").and(hasAnyDescendant(hasText(title))) - retry(catchBlock = { + retryWithIncreasingDelay(times = 10, catchBlock = { val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val y = device.displayHeight / 2 val x = device.displayWidth / 2 diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SmartSearchPreferencesPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SmartSearchPreferencesPage.kt index 8bfbd3ef0a..42fc873415 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SmartSearchPreferencesPage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SmartSearchPreferencesPage.kt @@ -40,6 +40,12 @@ class SmartSearchPreferencesPage(private val composeTestRule: ComposeTestRule) { } fun applyFilters() { + composeTestRule.onNodeWithTag("doneButton", useUnmergedTree = true) + .performClick() + composeTestRule.waitForIdle() + } + + fun cancelFilters() { composeTestRule.onNodeWithTag("navigationButton", useUnmergedTree = true) .performClick() composeTestRule.waitForIdle() diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/Endpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/Endpoint.kt similarity index 91% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/Endpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/Endpoint.kt index 1f8b364b21..a5b60b11d0 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/Endpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/Endpoint.kt @@ -16,11 +16,17 @@ */ @file:Suppress("unused") -package com.instructure.canvas.espresso.mockCanvas +package com.instructure.canvas.espresso.mockcanvas -import com.instructure.canvas.espresso.mockCanvas.utils.* -import okhttp3.* +import com.instructure.canvas.espresso.mockcanvas.utils.AuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.CanvasAuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.SegmentQualifier +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody /** diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/HttpResponder.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/HttpResponder.kt similarity index 84% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/HttpResponder.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/HttpResponder.kt index eccbff1aea..f0553eed3d 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/HttpResponder.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/HttpResponder.kt @@ -14,9 +14,9 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas +package com.instructure.canvas.espresso.mockcanvas -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars import okhttp3.Request import okhttp3.Response @@ -32,6 +32,7 @@ class HttpResponder( ) { private var getMethod: (() -> Response)? = null + private var headMethod: (() -> Response)? = null private var postMethod: (() -> Response)? = null private var putMethod: (() -> Response)? = null private var deleteMethod: (() -> Response)? = null @@ -52,10 +53,15 @@ class HttpResponder( deleteMethod = onHandle } + fun HttpResponder.HEAD(onHandle: () -> Response) { + headMethod = onHandle + } + fun handle(): Response { val method = request.method return when { method == "GET" && getMethod != null -> getMethod!!() + method == "HEAD" && headMethod != null -> headMethod!!() method == "POST" && postMethod != null -> postMethod!!() method == "PUT" && putMethod != null -> putMethod!!() method == "DELETE" && deleteMethod != null -> deleteMethod!!() @@ -69,7 +75,8 @@ class HttpResponder( postMethod?.let { println("POST $current\n") } putMethod?.let { println("PUT $current\n") } deleteMethod?.let { println("DELETE $current\n") } - return getMethod != null || postMethod != null || putMethod != null || deleteMethod != null + headMethod?.let { println("HEAD $current\n") } + return getMethod != null || postMethod != null || putMethod != null || deleteMethod != null || headMethod != null } } diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvas.kt similarity index 98% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvas.kt index a194421cf8..58ddd511fc 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvas.kt @@ -16,11 +16,11 @@ */ @file:Suppress("unused") -package com.instructure.canvas.espresso.mockCanvas +package com.instructure.canvas.espresso.mockcanvas import android.util.Log import com.github.javafaker.Faker -import com.instructure.canvas.espresso.mockCanvas.utils.Randomizer +import com.instructure.canvas.espresso.mockcanvas.utils.Randomizer import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.models.Account import com.instructure.canvasapi2.models.AccountNotification @@ -40,6 +40,7 @@ import com.instructure.canvasapi2.models.CanvasColor import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.CanvasContextPermission import com.instructure.canvasapi2.models.CanvasTheme +import com.instructure.canvasapi2.models.Checkpoint import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings @@ -341,6 +342,8 @@ class MockCanvas { /** Sets whether dashboard_cards returns true or false for isK5Subject field. */ var elementarySubjectPages: Boolean = false + var isCareerExperience: Boolean = false + /** Returns the current auth token for the specified user. Returns null if no such token exists. */ fun tokenFor(user: User): String? { tokens.forEach { (token, userId) -> @@ -1104,29 +1107,40 @@ fun MockCanvas.addAssignment( withDescription: Boolean = false, gradingType: String = "percent", discussionTopicHeader: DiscussionTopicHeader? = null, - htmlUrl: String? = "" + htmlUrl: String? = "", + submission: Submission? = null, + checkpoints: List = emptyList() ) : Assignment { val assignmentId = newItemId() val submissionTypeListRawStrings = submissionTypeList.map { it.apiString } var assignment = Assignment( - id = assignmentId, - assignmentGroupId = assignmentGroupId, - courseId = courseId, - name = name, - submissionTypesRaw = submissionTypeListRawStrings, - lockInfo = lockInfo, - lockedForUser = lockInfo != null, - userSubmitted = userSubmitted, - dueAt = dueAt, - pointsPossible = pointsPossible.toDouble(), - description = description, - lockAt = lockAt, - unlockAt = unlockAt, - published = true, - allDates = listOf(AssignmentDueDate(id = newItemId(), dueAt = dueAt, lockAt = lockAt, unlockAt = unlockAt)), - gradingType = gradingType, - discussionTopicHeader = discussionTopicHeader, - htmlUrl = htmlUrl + id = assignmentId, + assignmentGroupId = assignmentGroupId, + courseId = courseId, + name = name, + submissionTypesRaw = submissionTypeListRawStrings, + lockInfo = lockInfo, + lockedForUser = lockInfo != null, + userSubmitted = userSubmitted, + dueAt = dueAt, + pointsPossible = pointsPossible.toDouble(), + description = description, + lockAt = lockAt, + unlockAt = unlockAt, + published = true, + allDates = listOf( + AssignmentDueDate( + id = newItemId(), + dueAt = dueAt, + lockAt = lockAt, + unlockAt = unlockAt + ) + ), + gradingType = gradingType, + discussionTopicHeader = discussionTopicHeader, + htmlUrl = htmlUrl, + submission = submission, + checkpoints = checkpoints ) if (isQuizzesNext) { @@ -1426,7 +1440,8 @@ fun MockCanvas.addPageToCourse( title: String = Randomizer.randomPageTitle(), body: String = Randomizer.randomPageBody(), published: Boolean = false, - groupId: Long? = null + groupId: Long? = null, + frontPage: Boolean = false ): Page { val page = Page( @@ -1434,7 +1449,8 @@ fun MockCanvas.addPageToCourse( url = url, title = title, body = body, - published = published + published = published, + frontPage = frontPage ) var list : MutableList? = null diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvasInterceptor.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvasInterceptor.kt similarity index 88% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvasInterceptor.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvasInterceptor.kt index ec50d41c5e..b402ba5277 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvasInterceptor.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/MockCanvasInterceptor.kt @@ -14,10 +14,10 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas +package com.instructure.canvas.espresso.mockcanvas -import com.instructure.canvas.espresso.mockCanvas.endpoints.RootEndpoint -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.endpoints.RootEndpoint +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars import okhttp3.Interceptor import okhttp3.Response diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AccountEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AccountEndpoints.kt similarity index 90% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AccountEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AccountEndpoints.kt index 9aae21cc58..e54ec630c3 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AccountEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AccountEndpoints.kt @@ -14,12 +14,24 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.* -import com.instructure.canvasapi2.models.* +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.AccountId +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.UserId +import com.instructure.canvas.espresso.mockcanvas.utils.successPaginatedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse +import com.instructure.canvasapi2.models.Account +import com.instructure.canvasapi2.models.AccountNotification +import com.instructure.canvasapi2.models.BecomeUserPermission +import com.instructure.canvasapi2.models.HelpLink +import com.instructure.canvasapi2.models.HelpLinks +import com.instructure.canvasapi2.models.LaunchDefinition /** * Endpoint that can return a list of [Account]s. We currently assume that only one account is supported at a time and that diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ApiEndpoint.kt similarity index 87% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ApiEndpoint.kt index d63c62dfc2..ef196dfbc5 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ApiEndpoint.kt @@ -14,25 +14,26 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints import android.util.Log import com.google.gson.Gson -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addPlannable -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.DontCareAuthModel -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.StringId -import com.instructure.canvas.espresso.mockCanvas.utils.getJsonFromRequestBody -import com.instructure.canvas.espresso.mockCanvas.utils.grabJsonFromMultiPartBody -import com.instructure.canvas.espresso.mockCanvas.utils.successRedirectWithHeader -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.user +import com.instructure.canvas.espresso.mockCanvas.endpoints.CareerEndpoint +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addPlannable +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.DontCareAuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.StringId +import com.instructure.canvas.espresso.mockcanvas.utils.getJsonFromRequestBody +import com.instructure.canvas.espresso.mockcanvas.utils.grabJsonFromMultiPartBody +import com.instructure.canvas.espresso.mockcanvas.utils.successRedirectWithHeader +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.user import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.ModuleContentDetails @@ -67,6 +68,7 @@ import java.util.Date * - `search` -> [SearchEndpoint] */ object ApiEndpoint : Endpoint( + Segment("career") to CareerEndpoint, Segment("courses") to CourseListEndpoint, Segment("users") to UserListEndpoint, Segment("accounts") to AccountListEndpoint, @@ -191,7 +193,7 @@ object ApiEndpoint : Endpoint( "assignment" -> data .assignments .values - .filter { a -> a.courseId in courseIds } + .filter { a -> a.courseId in courseIds && a.checkpoints.isEmpty() } .map { a -> ScheduleItem( itemId = a.id.toString(), @@ -204,6 +206,27 @@ object ApiEndpoint : Endpoint( ) } + "sub_assignment" -> data + .assignments + .values + .filter { a -> a.courseId in courseIds && a.checkpoints.isNotEmpty() } + .flatMap { a -> + a.checkpoints.map { checkpoint -> + ScheduleItem( + itemId = checkpoint.tag ?: "", + title = a.name, + description = a.description, + startAt = checkpoint.dueAt, + assignment = a.copy( + dueAt = checkpoint.dueAt, + pointsPossible = checkpoint.pointsPossible ?: a.pointsPossible + ), + contextCode = "course_${a.courseId}", + contextName = data.courses[a.courseId]?.name + ) + } + } + // default handler assumes "event" event type else -> { data.courseCalendarEvents.filter { it.key in courseIds }.values.flatten() + @@ -288,6 +311,29 @@ object ApiEndpoint : Endpoint( val plannerOverride = getJsonFromRequestBody(request.body) request.successResponse(plannerOverride!!) } + }, + Segment("items") to Endpoint { + GET { + val contextCodes = request.url.queryParameterValues("context_codes[]") + val startDate = request.url.queryParameter("start_date").toDate() + val endDate = request.url.queryParameter("end_date").toDate() + val filter = request.url.queryParameter("filter") + val courseIds = contextCodes + .filter { it?.startsWith("course_").orDefault() } + .map { it?.substringAfter("course_")?.toLong() } + val userIds = contextCodes + .filter { it?.startsWith("user_").orDefault() } + .map { it?.substringAfter("user_")?.toLong() } + + val plannerItems = data.todos.filter { + courseIds.contains(it.courseId) || userIds.contains(it.userId) + }.filter { + if (it.plannableDate == null) return@filter true + if (startDate == null || endDate == null) return@filter true + it.plannableDate.time in startDate.time..endDate.time + } + request.successResponse(plannerItems) + } } ), Segment("features") to Endpoint( diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AssignmentEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AssignmentEndpoints.kt similarity index 93% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AssignmentEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AssignmentEndpoints.kt index 114413018a..6b9a62e654 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AssignmentEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/AssignmentEndpoints.kt @@ -13,19 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.AssignmentGroup import com.instructure.canvasapi2.models.GradeableStudent import com.instructure.canvasapi2.models.ObserveeAssignment -import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.models.ObserveeAssignmentGroup import com.instructure.canvasapi2.models.SubmissionSummary import okio.Buffer @@ -219,5 +218,6 @@ private fun Assignment.toObserveeAssignment() = ObserveeAssignment( moderatedGrading = moderatedGrading, anonymousGrading = anonymousGrading, allowedAttempts = allowedAttempts, - isStudioEnabled = isStudioEnabled + isStudioEnabled = isStudioEnabled, + checkpoints = checkpoints, ) \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CanvadocEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CanvadocEndpoints.kt similarity index 83% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CanvadocEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CanvadocEndpoints.kt index 62178d4539..988ecd8299 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CanvadocEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CanvadocEndpoints.kt @@ -13,11 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.* +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.AnnotationId +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse import com.instructure.canvasapi2.models.canvadocs.CanvaDocAnnotationResponse /** diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CareerEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CareerEndpoints.kt new file mode 100644 index 0000000000..28f6dd32a1 --- /dev/null +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CareerEndpoints.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.canvas.espresso.mockCanvas.endpoints + +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.loginapi.login.viewmodel.Experience + +object CareerEndpoint: Endpoint( + Segment("experience_summary") to ExperienceSummaryEndpoint, +) + +object ExperienceSummaryEndpoint: Endpoint( + response = { + GET { + val experience = if (data.isCareerExperience) { + Experience.Career + } else { + Experience.Academic(data.elementarySubjectPages) + } + request.successResponse(experience) + } + } +) \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ConversationEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ConversationEndpoints.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ConversationEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ConversationEndpoints.kt index 470f4519e5..592c62c1fb 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ConversationEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ConversationEndpoints.kt @@ -14,12 +14,17 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.* +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.user import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.Message import com.instructure.canvasapi2.models.UnreadConversationCount @@ -27,7 +32,7 @@ import com.instructure.canvasapi2.utils.APIHelper import okhttp3.FormBody import okhttp3.Request import okhttp3.Response -import java.util.* +import java.util.GregorianCalendar /** * Endpoint that can return a list of [Conversation]s diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CourseEndpoints.kt similarity index 96% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CourseEndpoints.kt index 48c7ca5ae9..8c351bf16c 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CourseEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CourseEndpoints.kt @@ -14,31 +14,31 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints import android.util.Log import com.google.gson.Gson -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addQuizSubmission -import com.instructure.canvas.espresso.mockCanvas.addReplyToDiscussion -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.AuthModel -import com.instructure.canvas.espresso.mockCanvas.utils.DontCareAuthModel -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.StringId -import com.instructure.canvas.espresso.mockCanvas.utils.UserId -import com.instructure.canvas.espresso.mockCanvas.utils.getJsonFromRequestBody -import com.instructure.canvas.espresso.mockCanvas.utils.grabJsonFromMultiPartBody -import com.instructure.canvas.espresso.mockCanvas.utils.noContentResponse -import com.instructure.canvas.espresso.mockCanvas.utils.successPaginatedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.user +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockcanvas.addFileToCourse +import com.instructure.canvas.espresso.mockcanvas.addQuizSubmission +import com.instructure.canvas.espresso.mockcanvas.addReplyToDiscussion +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.AuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.DontCareAuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.StringId +import com.instructure.canvas.espresso.mockcanvas.utils.UserId +import com.instructure.canvas.espresso.mockcanvas.utils.getJsonFromRequestBody +import com.instructure.canvas.espresso.mockcanvas.utils.grabJsonFromMultiPartBody +import com.instructure.canvas.espresso.mockcanvas.utils.noContentResponse +import com.instructure.canvas.espresso.mockcanvas.utils.successPaginatedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.user import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings @@ -116,6 +116,7 @@ object CourseListEndpoint : Endpoint( object CourseEndpoint : Endpoint( Segment("tabs") to CourseTabsEndpoint, Segment("assignments") to AssignmentIndexEndpoint, + Segment("front_page") to CourseFrontPageEndpoint, Segment("assignment_groups") to AssignmentGroupListEndpoint, Segment("external_tools") to ExternalToolsEndpoint, Segment("pages") to CoursePagesEndpoint, @@ -387,6 +388,25 @@ object CoursePagesEndpoint : Endpoint( } }) +/** + * Endpoint that returns the front page for a course + */ +object CourseFrontPageEndpoint : Endpoint( + response = { + GET { + val courseId = pathVars.courseId + val pages = data.coursePages[courseId] + val front = pages?.firstOrNull { it.frontPage } + if (front != null) { + request.successResponse(front) + } else { + request.unauthorizedResponse() + } + } + } +) + + /** * Endpoint that returns a specific page from a course */ diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CoursepermissionsEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CoursepermissionsEndpoint.kt similarity index 81% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CoursepermissionsEndpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CoursepermissionsEndpoint.kt index 1e522a9061..be4fce3d98 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/CoursepermissionsEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/CoursepermissionsEndpoint.kt @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse /** * Endpoint that can return a CanvasContextPermission value based on the course id diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/EnrollmentEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/EnrollmentEndpoint.kt similarity index 82% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/EnrollmentEndpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/EnrollmentEndpoint.kt index 333f7756b2..e3b824e963 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/EnrollmentEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/EnrollmentEndpoint.kt @@ -14,14 +14,13 @@ * along with this program. If not, see . */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.MockCanvas.Companion.data -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse import com.instructure.canvasapi2.apis.EnrollmentAPI object EnrollmentIndexEndpoint : Endpoint( diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ExternalToolsEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ExternalToolsEndpoints.kt similarity index 78% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ExternalToolsEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ExternalToolsEndpoints.kt index 7529f99f3e..841a377323 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ExternalToolsEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ExternalToolsEndpoints.kt @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse object ExternalToolsEndpoint : Endpoint( response = { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/FileEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/FileEndpoints.kt similarity index 65% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/FileEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/FileEndpoints.kt index add639fa4d..c204e5e7b1 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/FileEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/FileEndpoints.kt @@ -14,19 +14,17 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import android.util.Log -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.AuthModel -import com.instructure.canvas.espresso.mockCanvas.utils.DontCareAuthModel -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.successResponseRaw -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.AuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.DontCareAuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponseRaw +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse /** * Endpoint for file list operations @@ -56,6 +54,15 @@ object FileDownloadEndpoint : Endpoint ( request.unauthorizedResponse() } } + HEAD { + val fileId = pathVars.fileId + val content = data.fileContents[fileId] + if (content != null) { + request.successResponseRaw(content) + } else { + request.unauthorizedResponse() + } + } } diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/FolderEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/FolderEndpoints.kt similarity index 80% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/FolderEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/FolderEndpoints.kt index 58bef282c1..cb2eac1d74 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/FolderEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/FolderEndpoints.kt @@ -14,18 +14,17 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import android.util.Log -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.AuthModel -import com.instructure.canvas.espresso.mockCanvas.utils.DontCareAuthModel -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.successPaginatedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.AuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.DontCareAuthModel +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successPaginatedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse import com.instructure.canvasapi2.models.FileFolder /** diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/MiscEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/MiscEndpoints.kt similarity index 85% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/MiscEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/MiscEndpoints.kt index c031049a15..65066de9b0 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/MiscEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/MiscEndpoints.kt @@ -14,14 +14,13 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.utils.successPaginatedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.successPaginatedResponse import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.models.DashboardCard -import java.util.* +import java.util.Date /** * Endpoint that can return a list of DashboardCards for the request user diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/OAuthEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/OAuthEndpoint.kt similarity index 86% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/OAuthEndpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/OAuthEndpoint.kt index bf84a6181d..a3064aadb0 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/OAuthEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/OAuthEndpoint.kt @@ -14,9 +14,9 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.Endpoint /** * Endpoint for handling OAuth-related requests diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ObserverAlertsEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ObserverAlertsEndpoint.kt similarity index 82% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ObserverAlertsEndpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ObserverAlertsEndpoint.kt index 616df09ae3..591aaa4e37 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ObserverAlertsEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/ObserverAlertsEndpoint.kt @@ -12,14 +12,14 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */ package com.instructure.canvas.espresso.mockCanvas.endpoints + */ package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.StringId -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.StringId +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse import com.instructure.canvasapi2.models.Alert import com.instructure.canvasapi2.models.AlertWorkflowState diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/RootEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/RootEndpoint.kt similarity index 86% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/RootEndpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/RootEndpoint.kt index e3eb5f962d..c86481caf4 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/RootEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/RootEndpoint.kt @@ -1,12 +1,12 @@ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints import android.util.Log -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse import com.instructure.canvasapi2.models.AuthenticatedSession import okhttp3.Request import okhttp3.Response diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/SearchEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/SearchEndpoint.kt similarity index 88% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/SearchEndpoint.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/SearchEndpoint.kt index 384150a8ab..b331181483 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/SearchEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/SearchEndpoint.kt @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse /** * Base endpoint for searches diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/SubmissionEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/SubmissionEndpoints.kt similarity index 95% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/SubmissionEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/SubmissionEndpoints.kt index b0bd98abbb..f10343f06e 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/SubmissionEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/SubmissionEndpoints.kt @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.endpoints +package com.instructure.canvas.espresso.mockcanvas.endpoints import android.util.Log -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.UserId -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.user +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.addSubmissionForAssignment +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.UserId +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.user import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.Author import com.instructure.canvasapi2.models.FileUploadParams diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/UserEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/UserEndpoints.kt similarity index 95% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/UserEndpoints.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/UserEndpoints.kt index 89c10000ec..96c8b9ecd9 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/UserEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/endpoints/UserEndpoints.kt @@ -14,21 +14,21 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.endpoints - -import com.instructure.canvas.espresso.mockCanvas.Endpoint -import com.instructure.canvas.espresso.mockCanvas.addEnrollment -import com.instructure.canvas.espresso.mockCanvas.addFileToFolder -import com.instructure.canvas.espresso.mockCanvas.addObserverAlertThreshold -import com.instructure.canvas.espresso.mockCanvas.endpoint -import com.instructure.canvas.espresso.mockCanvas.utils.LongId -import com.instructure.canvas.espresso.mockCanvas.utils.PathVars -import com.instructure.canvas.espresso.mockCanvas.utils.Segment -import com.instructure.canvas.espresso.mockCanvas.utils.UserId -import com.instructure.canvas.espresso.mockCanvas.utils.successPaginatedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.successResponse -import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse -import com.instructure.canvas.espresso.mockCanvas.utils.user +package com.instructure.canvas.espresso.mockcanvas.endpoints + +import com.instructure.canvas.espresso.mockcanvas.Endpoint +import com.instructure.canvas.espresso.mockcanvas.addEnrollment +import com.instructure.canvas.espresso.mockcanvas.addFileToFolder +import com.instructure.canvas.espresso.mockcanvas.addObserverAlertThreshold +import com.instructure.canvas.espresso.mockcanvas.endpoint +import com.instructure.canvas.espresso.mockcanvas.utils.LongId +import com.instructure.canvas.espresso.mockcanvas.utils.PathVars +import com.instructure.canvas.espresso.mockcanvas.utils.Segment +import com.instructure.canvas.espresso.mockcanvas.utils.UserId +import com.instructure.canvas.espresso.mockcanvas.utils.successPaginatedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.successResponse +import com.instructure.canvas.espresso.mockcanvas.utils.unauthorizedResponse +import com.instructure.canvas.espresso.mockcanvas.utils.user import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Bookmark import com.instructure.canvasapi2.models.CanvasContext diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeAssignmentDetailsManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeAssignmentDetailsManager.kt similarity index 92% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeAssignmentDetailsManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeAssignmentDetailsManager.kt index c894c0c6dc..5638887a9d 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeAssignmentDetailsManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeAssignmentDetailsManager.kt @@ -12,9 +12,9 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */package com.instructure.canvas.espresso.mockCanvas.fakes + */package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.AssignmentDetailsQuery import com.instructure.canvasapi2.managers.graphql.AssignmentDetailsManager diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeCommentLibraryManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeCommentLibraryManager.kt similarity index 89% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeCommentLibraryManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeCommentLibraryManager.kt index 2d8d6180dd..bdc9cd200b 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeCommentLibraryManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeCommentLibraryManager.kt @@ -14,9 +14,9 @@ * along with this program. If not, see . * */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.managers.CommentLibraryManager class FakeCommentLibraryManager : CommentLibraryManager { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeCustomGradeStatusesManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeCustomGradeStatusesManager.kt similarity index 96% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeCustomGradeStatusesManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeCustomGradeStatusesManager.kt index f2441e78ce..f78f42cd73 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeCustomGradeStatusesManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeCustomGradeStatusesManager.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes import com.instructure.canvasapi2.CustomGradeStatusesQuery import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeDifferentiationTagsManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeDifferentiationTagsManager.kt new file mode 100644 index 0000000000..d9180f2e55 --- /dev/null +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeDifferentiationTagsManager.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.instructure.canvas.espresso.mockcanvas.fakes + +import com.instructure.canvasapi2.DifferentiationTagsQuery +import com.instructure.canvasapi2.managers.graphql.DifferentiationTagsManager + +class FakeDifferentiationTagsManager( + private val tags: List = emptyList(), + private val groupSetName: String? = null +) : DifferentiationTagsManager { + override suspend fun getDifferentiationTags( + courseId: Long, + forceNetwork: Boolean + ): DifferentiationTagsQuery.Data? { + if (tags.isEmpty()) { + return null + } + + // Use the first tag's name as the group set name if not specified + // This ensures no subtitle appears when group set name == group name + val setName = groupSetName ?: tags.firstOrNull()?.name ?: "Test Group Set" + + return DifferentiationTagsQuery.Data( + course = DifferentiationTagsQuery.Course( + groupSets = listOf( + DifferentiationTagsQuery.GroupSet( + _id = "1", + name = setName, + nonCollaborative = true, + groups = tags + ) + ) + ) + ) + } +} \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeGetHorizonCourseManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeGetHorizonCourseManager.kt new file mode 100644 index 0000000000..207a909deb --- /dev/null +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeGetHorizonCourseManager.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.canvas.espresso.mockcanvas.fakes + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvasapi2.GetCoursesQuery +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithModuleItemDurations +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.type.EnrollmentWorkflowState +import com.instructure.canvasapi2.utils.DataResult +import java.util.Date + +class FakeGetHorizonCourseManager(): HorizonGetCoursesManager { + override suspend fun getCoursesWithProgress( + userId: Long, + forceNetwork: Boolean + ): DataResult> { + return DataResult.Success(getCourses()) + } + + override suspend fun getEnrollments( + userId: Long, + forceNetwork: Boolean + ): DataResult> { + val activeCourse = getCourses()[0] + val completedCourse = getCourses()[1] + val invitedCourse = getCourses()[2] + return DataResult.Success( + listOf( + GetCoursesQuery.Enrollment( + id = MockCanvas.data.enrollments.values.toList()[0].id.toString(), + state = EnrollmentWorkflowState.active, + lastActivityAt = Date(), + course = GetCoursesQuery.Course( + id = activeCourse.courseId.toString(), + name = activeCourse.courseName, + image_download_url = null, + syllabus_body = activeCourse.courseSyllabus, + account = GetCoursesQuery.Account( + "Account 1" + ), + usersConnection = null + ) + ), + GetCoursesQuery.Enrollment( + id = MockCanvas.data.enrollments.values.toList()[1].id.toString(), + state = EnrollmentWorkflowState.completed, + lastActivityAt = Date(), + course = GetCoursesQuery.Course( + id = completedCourse.courseId.toString(), + name = completedCourse.courseName, + image_download_url = null, + syllabus_body = completedCourse.courseSyllabus, + account = GetCoursesQuery.Account( + "Account 1" + ), + usersConnection = null + ) + ) + ) + ) + } + + override suspend fun getProgramCourses( + courseId: Long, + forceNetwork: Boolean + ): DataResult { + return DataResult.Success( + CourseWithModuleItemDurations( + courseId = courseId, + courseName = "Program Course", + ) + ) + } + + fun getCourses(): List { + val courses = MockCanvas.data.courses.values.toList() + val activeCourse = CourseWithProgress( + courseId = courses[0].id, + courseName = courses[0].name, + courseSyllabus = "Syllabus for Course 1", + progress = 0.25 + ) + val completedCourse = CourseWithProgress( + courseId = courses[1].id, + courseName = courses[1].name, + courseSyllabus = "Syllabus for Course 2", + progress = 1.0 + ) + val invitedCourse = CourseWithProgress( + courseId = courses[2].id, + courseName = courses[2].name, + courseSyllabus = null, + progress = 0.0 + ) + + return listOf(activeCourse, completedCourse, invitedCourse) + } +} \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeInboxSettingsManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeInboxSettingsManager.kt similarity index 92% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeInboxSettingsManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeInboxSettingsManager.kt index f9d75edff5..f6ade0b4ed 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeInboxSettingsManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeInboxSettingsManager.kt @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.managers.InboxSettingsManager import com.instructure.canvasapi2.managers.InboxSignatureSettings import com.instructure.canvasapi2.utils.DataResult diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeJourneyApiManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeJourneyApiManager.kt new file mode 100644 index 0000000000..ae1c4ffdfe --- /dev/null +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeJourneyApiManager.kt @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.canvas.espresso.mockcanvas.fakes + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetProgramsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetSkillsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetWidgetsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Program +import com.instructure.canvasapi2.managers.graphql.horizon.journey.ProgramRequirement +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Skill +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.journey.GetWidgetDataQuery +import com.instructure.journey.type.ProgramProgressCourseEnrollmentStatus +import com.instructure.journey.type.ProgramVariantType +import java.util.Date + +class FakeGetProgramsManager : GetProgramsManager { + override suspend fun getPrograms(forceNetwork: Boolean): List { + return getProgramsData() + } + + override suspend fun getProgramById(programId: String, forceNetwork: Boolean): Program { + return getProgramsData().first { it.id == programId } + } + + override suspend fun enrollCourse(progressId: String): DataResult { + return if (getProgramsData().first().sortedRequirements.any { it.progressId == progressId }) { + DataResult.Success(Unit) + } else { + DataResult.Fail() + } + } + + private fun getProgramsData(): List { + val program1 = Program( + id = "1", + name = "Program 1", + description = "Description for Program 1", + sortedRequirements = listOf( + ProgramRequirement( + id = "1", + progressId = "1", + progress = 50.0, + courseId = MockCanvas.data.courses.values.toList()[0].id, + required = true, + enrollmentStatus = ProgramProgressCourseEnrollmentStatus.ENROLLED, + ) + ), + startDate = null, + endDate = null, + variant = ProgramVariantType.LINEAR + ) + val program2 = Program( + id = "2", + name = "Program 2", + description = "Description for Program 2", + sortedRequirements = emptyList(), + startDate = null, + endDate = null, + variant = ProgramVariantType.NON_LINEAR + ) + return listOf(program1, program2) + } +} + +class FakeGetWidgetsManager : GetWidgetsManager { + override suspend fun getTimeSpentWidgetData(courseId: Long?, forceNetwork: Boolean): GetWidgetDataQuery.WidgetData { + return GetWidgetDataQuery.WidgetData( + lastModifiedDate = Date(), + data = listOf( + mapOf( + "date" to "2025-10-08", + "user_id" to 1.0, + "course_id" to 101.0, + "course_name" to "Test Course", + "minutes_per_day" to 600.0 + ) + ) + ) + } + + override suspend fun getLearningStatusWidgetData( + courseId: Long?, + forceNetwork: Boolean + ): GetWidgetDataQuery.WidgetData { + return GetWidgetDataQuery.WidgetData( + lastModifiedDate = Date(), + data = listOf( + mapOf("module_count_completed" to 5) + ) + ) + } +} + +class FakeGetSkillsManager: GetSkillsManager { + override suspend fun getSkills( + completedOnly: Boolean?, + forceNetwork: Boolean + ): List { + return listOf( + Skill( + id = "1", + name = "Skill 1", + proficiencyLevel = "beginner", + createdAt = null, + updatedAt = null + ), + Skill( + id = "2", + name = "Skill 2", + proficiencyLevel = "expert", + createdAt = null, + updatedAt = null + ) + ) + } + +} \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeModuleManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeModuleManager.kt new file mode 100644 index 0000000000..e40370129c --- /dev/null +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeModuleManager.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.canvas.espresso.mockcanvas.fakes + +import com.instructure.canvasapi2.managers.graphql.ModuleItemCheckpoint +import com.instructure.canvasapi2.managers.graphql.ModuleItemWithCheckpoints +import com.instructure.canvasapi2.managers.graphql.ModuleManager + +class FakeModuleManager : ModuleManager { + + var checkpointsMap: Map> = emptyMap() + + override suspend fun getModuleItemCheckpoints(courseId: String, forceNetwork: Boolean): List { + return checkpointsMap[courseId] ?: emptyList() + } + + fun setCheckpoints(courseId: String, moduleItemId: String, checkpoints: List) { + val moduleItemWithCheckpoints = ModuleItemWithCheckpoints( + moduleItemId = moduleItemId, + checkpoints = checkpoints + ) + checkpointsMap = checkpointsMap + (courseId to listOf(moduleItemWithCheckpoints)) + } +} \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakePostPolicyManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakePostPolicyManager.kt similarity index 96% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakePostPolicyManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakePostPolicyManager.kt index 4d0486ca15..69c3becfa0 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakePostPolicyManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakePostPolicyManager.kt @@ -12,7 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */package com.instructure.canvas.espresso.mockCanvas.fakes + */package com.instructure.canvas.espresso.mockcanvas.fakes import com.instructure.canvasapi2.HideAssignmentGradesForSectionsMutation import com.instructure.canvasapi2.HideAssignmentGradesMutation diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeStudentContextManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeStudentContextManager.kt similarity index 97% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeStudentContextManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeStudentContextManager.kt index 08b7523101..22f18cc65a 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeStudentContextManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeStudentContextManager.kt @@ -14,9 +14,9 @@ * along with this program. If not, see . * */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.StudentContextCardQuery import com.instructure.canvasapi2.managers.StudentContextManager import com.instructure.canvasapi2.type.AssignmentState diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionCommentsManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionCommentsManager.kt similarity index 96% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionCommentsManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionCommentsManager.kt index 42654b20c1..1b18558644 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionCommentsManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionCommentsManager.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes import com.instructure.canvasapi2.CreateSubmissionCommentMutation import com.instructure.canvasapi2.SubmissionCommentsQuery diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionContentManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionContentManager.kt similarity index 96% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionContentManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionContentManager.kt index e8910ef9e5..34fee6460f 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionContentManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionContentManager.kt @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.SubmissionContentQuery import com.instructure.canvasapi2.fragment.SubmissionFields import com.instructure.canvasapi2.managers.graphql.SubmissionContentManager @@ -26,7 +26,8 @@ import com.instructure.canvasapi2.type.SubmissionType class FakeSubmissionContentManager : SubmissionContentManager { override suspend fun getSubmissionContent( userId: Long, - assignmentId: Long + assignmentId: Long, + domain: String? ): SubmissionContentQuery.Data { val assignment = MockCanvas.data.assignments[assignmentId] val submission = MockCanvas.data.submissions[assignmentId]?.get(0) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionDetailsManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionDetailsManager.kt similarity index 94% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionDetailsManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionDetailsManager.kt index 5e1c023c7b..e99be5fda8 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionDetailsManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionDetailsManager.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.SubmissionDetailsQuery import com.instructure.canvasapi2.managers.graphql.SubmissionDetailsManager import com.instructure.canvasapi2.type.SubmissionType diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionGradeManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionGradeManager.kt similarity index 96% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionGradeManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionGradeManager.kt index 6e74881772..916c727b6c 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionGradeManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionGradeManager.kt @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.SubmissionGradeQuery import com.instructure.canvasapi2.UpdateSubmissionGradeMutation import com.instructure.canvasapi2.UpdateSubmissionStatusMutation @@ -30,7 +30,8 @@ class FakeSubmissionGradeManager : SubmissionGradeManager { override suspend fun getSubmissionGrade( assignmentId: Long, studentId: Long, - forceNetwork: Boolean + forceNetwork: Boolean, + domain: String? ): SubmissionGradeQuery.Data { val assignment = MockCanvas.data.assignments[assignmentId] val course = MockCanvas.data.courses[assignment?.courseId] diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionRubricManager.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionRubricManager.kt similarity index 96% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionRubricManager.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionRubricManager.kt index a6a54f4ece..303a92b766 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/fakes/FakeSubmissionRubricManager.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/fakes/FakeSubmissionRubricManager.kt @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvas.espresso.mockCanvas.fakes +package com.instructure.canvas.espresso.mockcanvas.fakes -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.SubmissionRubricQuery import com.instructure.canvasapi2.managers.SubmissionRubricManager diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/AuthUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/AuthUtils.kt similarity index 96% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/AuthUtils.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/AuthUtils.kt index 42fb9644eb..fee4537a78 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/AuthUtils.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/AuthUtils.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.utils +package com.instructure.canvas.espresso.mockcanvas.utils import com.instructure.canvasapi2.utils.validOrNull import okhttp3.Request diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/PathUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/PathUtils.kt similarity index 97% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/PathUtils.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/PathUtils.kt index 85332493f7..0c4cf75ad4 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/PathUtils.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/PathUtils.kt @@ -14,9 +14,9 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.utils +package com.instructure.canvas.espresso.mockcanvas.utils -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import okhttp3.Request import java.net.URLEncoder import kotlin.reflect.KMutableProperty1 diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/Randomizer.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/Randomizer.kt similarity index 97% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/Randomizer.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/Randomizer.kt index faf823f0a4..d981765e1e 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/Randomizer.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/Randomizer.kt @@ -16,10 +16,11 @@ */ @file:Suppress("unused") -package com.instructure.canvas.espresso.mockCanvas.utils +package com.instructure.canvas.espresso.mockcanvas.utils import com.github.javafaker.Faker -import java.util.* +import java.util.Date +import java.util.UUID /** * Provides access to numerous convenience functions for generating random data diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/RequestUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/RequestUtils.kt similarity index 97% rename from automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/RequestUtils.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/RequestUtils.kt index cda02e7865..ea9cab764a 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/RequestUtils.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockcanvas/utils/RequestUtils.kt @@ -14,15 +14,18 @@ * limitations under the License. * */ -package com.instructure.canvas.espresso.mockCanvas.utils +package com.instructure.canvas.espresso.mockcanvas.utils import android.util.Log import com.google.gson.Gson import com.google.gson.JsonObject -import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.MockCanvas import com.instructure.canvasapi2.models.User -import okhttp3.* import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody import okio.Buffer import okio.IOException diff --git a/automation/espresso/src/main/kotlin/com/instructure/composeTest/ComposeCustomMatchers.kt b/automation/espresso/src/main/kotlin/com/instructure/composetest/ComposeCustomMatchers.kt similarity index 97% rename from automation/espresso/src/main/kotlin/com/instructure/composeTest/ComposeCustomMatchers.kt rename to automation/espresso/src/main/kotlin/com/instructure/composetest/ComposeCustomMatchers.kt index fc1d40bd4b..868977b0d1 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/composeTest/ComposeCustomMatchers.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/composetest/ComposeCustomMatchers.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.composeTest +package com.instructure.composetest import androidx.annotation.DrawableRes import androidx.compose.ui.semantics.getOrNull diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt index 0e5873edff..3bb02d11a9 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt @@ -16,9 +16,11 @@ package com.instructure.espresso import android.os.Build +import android.util.Log import android.view.View import androidx.annotation.RequiresApi import androidx.recyclerview.widget.RecyclerView +import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.NoMatchingViewException @@ -29,6 +31,9 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.web.sugar.Web import androidx.test.espresso.web.webdriver.DriverAtoms import androidx.test.espresso.web.webdriver.Locator +import androidx.test.platform.app.InstrumentationRegistry +import androidx.work.WorkManager +import com.instructure.canvas.espresso.TestAppManager import com.instructure.espresso.page.plus import com.instructure.pandautils.binding.BindableViewHolder import org.apache.commons.lang3.StringUtils @@ -241,3 +246,61 @@ fun getRecyclerViewFromMatcher(matcher: Matcher): RecyclerView { return recyclerView ?: throw IllegalStateException("Failed to retrieve RecyclerView") } +fun handleWorkManagerTask(workerTag: String, timeoutMillis: Long = 20000) { + val app = ApplicationProvider.getApplicationContext() + var endTime = System.currentTimeMillis() + timeoutMillis + var workInfo: androidx.work.WorkInfo? = null + + var testDriver = app.testDriver + if (testDriver == null) { + while (System.currentTimeMillis() < endTime && app.testDriver == null) { + Log.w("handleWorkManagerTask", "testDriver is null, attempting to initialize WorkManager") + app.initWorkManager(app) + testDriver = app.testDriver + Thread.sleep(500) + } + } + + if (testDriver == null) { + Assert.fail("A TestAppManager.testDriver was null, so was not initialized before the timeout.") + } + + endTime = System.currentTimeMillis() + timeoutMillis + while (System.currentTimeMillis() < endTime) { + try { + val workInfos = WorkManager.getInstance(app).getWorkInfosByTag(workerTag).get() + for(work in workInfos) { + Log.w("STUDENT_APP_TAG","WorkInfo: $work") + } + workInfo = workInfos.find { !it.state.isFinished } + + if (workInfo != null) break + + } catch (e: Exception) { + e.printStackTrace() + } + Thread.sleep(500) + } + + if (workInfo == null) { + val workInfos = WorkManager.getInstance(app).getWorkInfosByTag(workerTag).get() + Assert.fail("Unable to find WorkInfo with tag:'$workerTag' in ${timeoutMillis} ms. WorkInfos found: $workInfos") + } + + testDriver!!.setAllConstraintsMet(workInfo!!.id) + waitForWorkManagerJobsToFinish(workerTag = workerTag) +} + + +private fun waitForWorkManagerJobsToFinish(timeoutMs: Long = 20000L, workerTag: String) { + val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext + val workManager = WorkManager.getInstance(context) + val start = System.currentTimeMillis() + while (true) { + val unfinished = workManager.getWorkInfosByTag(workerTag).get() + .any { !it.state.isFinished } + if (!unfinished) break + if (System.currentTimeMillis() - start > timeoutMs) break + Thread.sleep(250) + } +} diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/ViewUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/ViewUtils.kt deleted file mode 100644 index 4f382966bc..0000000000 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/ViewUtils.kt +++ /dev/null @@ -1,34 +0,0 @@ -// -// Copyright (C) 2018-present Instructure, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -package com.instructure.espresso - -import androidx.test.espresso.util.HumanReadables -import android.view.View -import androidx.test.espresso.Espresso - -object ViewUtils { - - fun toString(view: View): String { - return HumanReadables.getViewHierarchyErrorMessage(view, null, "", null) - } - - fun pressBackButton(times: Int) { - for(i in 1..times) { - Espresso.pressBack() - } - } -} diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P0.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P0.kt deleted file mode 100644 index ac23170c8a..0000000000 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P0.kt +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (C) 2018-present Instructure, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - - -package com.instructure.espresso.filters - -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy - -@Retention(RetentionPolicy.RUNTIME) -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CLASS, AnnotationTarget.FILE) -annotation class P0 diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P1.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P1.kt deleted file mode 100644 index 2e15cc3f40..0000000000 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P1.kt +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (C) 2018-present Instructure, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - - -package com.instructure.espresso.filters - -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy - -@Retention(RetentionPolicy.RUNTIME) -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CLASS, AnnotationTarget.FILE) -annotation class P1 diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P2.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P2.kt deleted file mode 100644 index 0c16b356dc..0000000000 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P2.kt +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (C) 2018-present Instructure, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - - -package com.instructure.espresso.filters - -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy - -@Retention(RetentionPolicy.RUNTIME) -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CLASS, AnnotationTarget.FILE) -annotation class P2 diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P3.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P3.kt deleted file mode 100644 index 22ba81587b..0000000000 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P3.kt +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (C) 2018-present Instructure, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - - -package com.instructure.espresso.filters - -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy - -@Retention(RetentionPolicy.RUNTIME) -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CLASS, AnnotationTarget.FILE) -annotation class P3 diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P4.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P4.kt deleted file mode 100644 index a8febaebd3..0000000000 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/filters/P4.kt +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (C) 2018-present Instructure, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - - -package com.instructure.espresso.filters - -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy - -@Retention(RetentionPolicy.RUNTIME) -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CLASS, AnnotationTarget.FILE) -annotation class P4 diff --git a/automation/soseedygrpc/build.gradle b/automation/soseedygrpc/build.gradle index d83c2030fb..ba6cdfef85 100644 --- a/automation/soseedygrpc/build.gradle +++ b/automation/soseedygrpc/build.gradle @@ -23,7 +23,7 @@ dependencies { compile project(':dataseedingapi') // https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22io.grpc%22%20a%3A%22grpc-netty%22 - compile 'io.grpc:grpc-netty:1.61.1' + compile 'io.grpc:grpc-netty:1.75.0' compile Libs.KOTLIN_STD_LIB // https://mvnrepository.com/artifact/io.netty/netty-tcnative-boringssl-static diff --git a/gradle/gradle/wrapper/gradle-wrapper.properties b/gradle/gradle/wrapper/gradle-wrapper.properties index b82aa23a4f..37f853b1c8 100644 --- a/gradle/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle index da2c33a4fc..ca8edd7eba 100644 --- a/gradle/jacoco.gradle +++ b/gradle/jacoco.gradle @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -def coveredProject = subprojects.findAll { project -> project.name == 'student' || project.name == 'teacher' || project.name == 'pandautils'} +def coveredProject = subprojects.findAll { project -> project.name == 'student' || project.name == 'teacher' || project.name == 'parent' || project.name == 'pandautils'} apply plugin: 'jacoco' diff --git a/libs/DocumentScanner/build.gradle b/libs/DocumentScanner/build.gradle deleted file mode 100644 index 98764308cc..0000000000 --- a/libs/DocumentScanner/build.gradle +++ /dev/null @@ -1,91 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -group="com.zynkware" - -def libraryVersionCode = 5 -def libraryVersionName = "1.0.1" - -repositories { - mavenCentral() - google() - maven { url "https://jitpack.io" } -} - -android { - namespace 'com.zynksoftware.documentscanner' - compileSdkVersion Versions.COMPILE_SDK - buildToolsVersion Versions.BUILD_TOOLS - - defaultConfig { - minSdkVersion 21 - targetSdkVersion Versions.TARGET_SDK - versionCode libraryVersionCode - versionName libraryVersionName - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17 - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - main.res.srcDirs = ['src/main/res'] - main.manifest.srcFile 'src/main/AndroidManifest.xml' - } - - buildFeatures { - viewBinding true - } -} - -repositories { - mavenCentral() - google() - maven { url 'https://jitpack.io' } -} - -dependencies { - implementation fileTree(dir: "libs", include: ["*.jar"]) - implementation Libs.KOTLIN_STD_LIB - - implementation 'androidx.core:core-ktx:1.12.0' - implementation Libs.ANDROIDX_APPCOMPAT - - implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' - - implementation 'com.github.zynkware:Tiny-OpenCV:4.4.0-3' - - implementation "androidx.camera:camera-camera2:1.4.2" - implementation "androidx.camera:camera-lifecycle:1.4.2" - implementation "androidx.camera:camera-view:1.4.2" - - implementation 'androidx.exifinterface:exifinterface:1.4.0' - implementation Libs.KOTLIN_COROUTINES_ANDROID - implementation 'id.zelory:compressor:3.0.1' -} - -task sourceJar(type: Jar) { - from android.sourceSets.main.java.srcDirs - from fileTree(dir: 'src/libs', include: ['*.jar']) -} - -task androidSourcesJar(type: Jar) { - archiveClassifier.set('sources') - from android.sourceSets.main.java.srcDirs -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/AndroidManifest.xml b/libs/DocumentScanner/src/main/AndroidManifest.xml deleted file mode 100644 index 679e8fc75a..0000000000 --- a/libs/DocumentScanner/src/main/AndroidManifest.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ScanActivity.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ScanActivity.kt deleted file mode 100644 index e388c63ca0..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ScanActivity.kt +++ /dev/null @@ -1,29 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner - -import com.zynksoftware.documentscanner.ui.scan.InternalScanActivity - -abstract class ScanActivity : InternalScanActivity() { - - fun addFragmentContentLayout() { - addFragmentContentLayoutInternal() - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/BitmapExtensions.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/BitmapExtensions.kt deleted file mode 100644 index 8afd1e1f98..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/BitmapExtensions.kt +++ /dev/null @@ -1,51 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.extensions - -import android.graphics.Bitmap -import android.graphics.Matrix -import android.graphics.RectF -import org.opencv.android.Utils -import org.opencv.core.CvType -import org.opencv.core.Mat -import org.opencv.core.Scalar - -internal fun Bitmap.rotateBitmap(angle: Int): Bitmap { - val matrix = Matrix() - matrix.postRotate(angle.toFloat()) - return Bitmap.createBitmap(this, 0, 0, this.width, this.height, matrix, true) -} - -internal fun Bitmap.toMat(): Mat { - val mat = Mat(this.height, this.width, CvType.CV_8U, Scalar(4.toDouble())) - val bitmap32 = this.copy(Bitmap.Config.ARGB_8888, true) - Utils.bitmapToMat(bitmap32, mat) - return mat -} - -internal fun Bitmap.scaledBitmap(width: Int, height: Int): Bitmap { - val m = Matrix() - m.setRectToRect( - RectF(0f, 0f, this.width.toFloat(), this.height.toFloat()), - RectF(0f, 0f, width.toFloat(), height.toFloat()), - Matrix.ScaleToFit.CENTER - ) - return Bitmap.createBitmap(this, 0, 0, this.width, this.height, m, true) -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/ImageProxyExtensions.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/ImageProxyExtensions.kt deleted file mode 100644 index f0b6716c1f..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/ImageProxyExtensions.kt +++ /dev/null @@ -1,93 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.extensions - -import android.graphics.ImageFormat -import androidx.camera.core.ImageProxy -import org.opencv.core.CvType -import org.opencv.core.Mat -import org.opencv.imgproc.Imgproc - -internal fun ImageProxy.yuvToRgba(): Mat { - val rgbaMat = Mat() - - if (format == ImageFormat.YUV_420_888 - && planes.size == 3) { - - val chromaPixelStride = planes[1].pixelStride - - if (chromaPixelStride == 2) { // Chroma channels are interleaved - val yPlane = planes[0].buffer - val uvPlane1 = planes[1].buffer - val uvPlane2 = planes[2].buffer - - val yMat = Mat((height), width, CvType.CV_8UC1, yPlane) - val uvMat1 = Mat(height / 2, width / 2, CvType.CV_8UC2, uvPlane1) - val uvMat2 = Mat(height / 2, width / 2, CvType.CV_8UC2, uvPlane2) - val addrDiff = uvMat2.dataAddr() - uvMat1.dataAddr() - if (addrDiff > 0) { - Imgproc.cvtColorTwoPlane(yMat, uvMat1, rgbaMat, Imgproc.COLOR_YUV2RGBA_NV12) - } else { - Imgproc.cvtColorTwoPlane(yMat, uvMat2, rgbaMat, Imgproc.COLOR_YUV2RGBA_NV21) - } - } else { // Chroma channels are not interleaved - val yuvBytes = ByteArray(width * (height + height / 2)) - val yPlane = planes[0].buffer - val uPlane = planes[1].buffer - val vPlane = planes[2].buffer - - yPlane.get(yuvBytes, 0, width * height) - - val chromaRowStride = planes[1].rowStride - val chromaRowPadding = chromaRowStride - width / 2 - - var offset = width * height - if (chromaRowPadding == 0) { - // When the row stride of the chroma channels equals their width, we can copy - // the entire channels in one go - uPlane.get(yuvBytes, offset, width * height / 4) - offset += width * height / 4 - vPlane.get(yuvBytes, offset, width * height / 4) - } else { - // When not equal, we need to copy the channels row by row - for (i in 0 until height / 2) { - uPlane.get(yuvBytes, offset, width / 2) - offset += width / 2 - if (i < height / 2 - 1) { - uPlane.position(uPlane.position() + chromaRowPadding) - } - } - for (i in 0 until height / 2) { - vPlane.get(yuvBytes, offset, width / 2) - offset += width / 2 - if (i < height / 2 - 1) { - vPlane.position(vPlane.position() + chromaRowPadding) - } - } - } - - val yuvMat = Mat(height + height / 2, width, CvType.CV_8UC1) - yuvMat.put(0, 0, yuvBytes) - Imgproc.cvtColor(yuvMat, rgbaMat, Imgproc.COLOR_YUV2BGR_NV21, 4) - } - } - - return rgbaMat -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/OpenCvExtensions.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/OpenCvExtensions.kt deleted file mode 100644 index 4d6e7f4779..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/OpenCvExtensions.kt +++ /dev/null @@ -1,42 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.extensions - -import android.graphics.Bitmap -import org.opencv.android.Utils -import org.opencv.core.* -import java.util.* - -internal fun Mat.toBitmap(): Bitmap { - val bitmap = Bitmap.createBitmap(this.cols(), this.rows(), Bitmap.Config.ARGB_8888) - Utils.matToBitmap(this, bitmap) - return bitmap -} - -internal fun MatOfPoint2f.scaleRectangle(scale: Double): MatOfPoint2f { - val originalPoints = this.toList() - val resultPoints: MutableList = ArrayList() - for (point in originalPoints) { - resultPoints.add(Point(point.x * scale, point.y * scale)) - } - val result = MatOfPoint2f() - result.fromList(resultPoints) - return result -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/ViewExtensions.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/ViewExtensions.kt deleted file mode 100644 index 17375b9edb..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/extensions/ViewExtensions.kt +++ /dev/null @@ -1,30 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.extensions - -import android.view.View - -internal fun View.hide() { - visibility = View.GONE -} - -internal fun View.show() { - visibility = View.VISIBLE -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/FileUriUtils.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/FileUriUtils.kt deleted file mode 100644 index f65058f84b..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/FileUriUtils.kt +++ /dev/null @@ -1,240 +0,0 @@ -package com.zynksoftware.documentscanner.common.utils - -import android.content.ContentUris -import android.content.Context -import android.database.Cursor -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.provider.DocumentsContract -import android.provider.MediaStore -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream - -/** - * This file was taken from - * https://gist.github.com/HBiSoft/15899990b8cd0723c3a894c1636550a8 - * - * Later on it was modified from the below resource: - * https://raw.githubusercontent.com/iPaulPro/aFileChooser/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java - * https://raw.githubusercontent.com/iPaulPro/aFileChooser/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java - */ - -internal object FileUriUtils { - - fun getRealPath(context: Context, uri: Uri): String? { - var path = getPathFromLocalUri(context, uri) - if (path == null) { - path = getPathFromRemoteUri(context, uri) - } - return path - } - - private fun getPathFromLocalUri(context: Context, uri: Uri): String? { - - val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT - - // DocumentProvider - if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { - // ExternalStorageProvider - if (isExternalStorageDocument(uri)) { - val docId = DocumentsContract.getDocumentId(uri) - val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - val type = split[0] - - // This is for checking Main Memory - return if ("primary".equals(type, ignoreCase = true)) { - if (split.size > 1) { - Environment.getExternalStorageDirectory().toString() + "/" + split[1] - } else { - Environment.getExternalStorageDirectory().toString() + "/" - } - // This is for checking SD Card - } else { - val path = "storage" + "/" + docId.replace(":", "/") - if (File(path).exists()) { - path - } else { - "/storage/sdcard/" + split[1] - } - } - } else if (isDownloadsDocument(uri)) { - val fileName = getFilePath(context, uri) - if (fileName != null) { - return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName - } - - val id = DocumentsContract.getDocumentId(uri) - val contentUri = ContentUris.withAppendedId( - Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id) - ) - return getDataColumn(context, contentUri, null, null) - } else if (isMediaDocument(uri)) { - val docId = DocumentsContract.getDocumentId(uri) - val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - val type = split[0] - - var contentUri: Uri? = null - if ("image" == type) { - contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI - } else if ("video" == type) { - contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI - } else if ("audio" == type) { - contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - } - - val selection = "_id=?" - val selectionArgs = arrayOf(split[1]) - - return getDataColumn(context, contentUri, selection, selectionArgs) - } // MediaProvider - // DownloadsProvider - } else if ("content".equals(uri.scheme!!, ignoreCase = true)) { - - // Return the remote address - return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(context, uri, null, null) - } else if ("file".equals(uri.scheme!!, ignoreCase = true)) { - return uri.path - } // File - // MediaStore (and general) - - return null - } - - private fun getDataColumn( - context: Context, - uri: Uri?, - selection: String?, - selectionArgs: Array? - ): String? { - - var cursor: Cursor? = null - val column = "_data" - val projection = arrayOf(column) - - try { - cursor = context.contentResolver.query(uri!!, projection, selection, selectionArgs, null) - if (cursor != null && cursor.moveToFirst()) { - val index = cursor.getColumnIndexOrThrow(column) - return cursor.getString(index) - } - } catch (ex: Exception) { - } finally { - cursor?.close() - } - return null - } - - private fun getFilePath(context: Context, uri: Uri): String? { - - var cursor: Cursor? = null - val projection = arrayOf(MediaStore.MediaColumns.DISPLAY_NAME) - - try { - cursor = context.contentResolver.query(uri, projection, null, null, null) - if (cursor != null && cursor.moveToFirst()) { - val index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME) - return cursor.getString(index) - } - } finally { - cursor?.close() - } - return null - } - - private fun getPathFromRemoteUri(context: Context, uri: Uri): String? { - // The code below is why Java now has try-with-resources and the Files utility. - var file: File? = null - var inputStream: InputStream? = null - var outputStream: OutputStream? = null - var success = false - try { - val extension = getImageExtension(uri) - inputStream = context.contentResolver.openInputStream(uri) - val storageDir = context.cacheDir - if (!storageDir.exists()) { - storageDir.mkdirs() - } - file = File(storageDir, "remotePicture${extension}") - file.createNewFile() - outputStream = FileOutputStream(file) - if (inputStream != null) { - inputStream.copyTo(outputStream, bufferSize = 4 * 1024) - success = true - } - } catch (ignored: IOException) { - } finally { - try { - inputStream?.close() - } catch (ignored: IOException) { - } - - try { - outputStream?.close() - } catch (ignored: IOException) { - // If closing the output stream fails, we cannot be sure that the - // target file was written in full. Flushing the stream merely moves - // the bytes into the OS, not necessarily to the file. - success = false - } - } - return if (success) file!!.path else null - } - - /** @return extension of image with dot, or default .jpg if it none. - */ - private fun getImageExtension(uriImage: Uri): String { - var extension: String? = null - - try { - val imagePath = uriImage.path - if (imagePath != null && imagePath.lastIndexOf(".") != -1) { - extension = imagePath.substring(imagePath.lastIndexOf(".") + 1) - } - } catch (e: Exception) { - extension = null - } - - if (extension == null || extension.isEmpty()) { - // default extension for matches the previous behavior of the plugin - extension = "jpg" - } - - return ".$extension" - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is ExternalStorageProvider. - */ - private fun isExternalStorageDocument(uri: Uri): Boolean { - return "com.android.externalstorage.documents" == uri.authority - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is DownloadsProvider. - */ - private fun isDownloadsDocument(uri: Uri): Boolean { - return "com.android.providers.downloads.documents" == uri.authority - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is MediaProvider. - */ - private fun isMediaDocument(uri: Uri): Boolean { - return "com.android.providers.media.documents" == uri.authority - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is Google Photos. - */ - private fun isGooglePhotosUri(uri: Uri): Boolean { - return "com.google.android.apps.photos.content" == uri.authority - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/ImageDetectionProperties.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/ImageDetectionProperties.kt deleted file mode 100644 index 39310e1df5..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/ImageDetectionProperties.kt +++ /dev/null @@ -1,81 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.utils - -import org.opencv.core.MatOfPoint2f -import org.opencv.core.Point -import kotlin.math.abs - -internal class ImageDetectionProperties( - private val previewWidth: Double, private val previewHeight: Double, - private val topLeftPoint: Point, private val bottomLeftPoint: Point, - private val bottomRightPoint: Point, private val topRightPoint: Point, - private val resultWidth: Int, private val resultHeight: Int -) { - - companion object { - private const val SMALLEST_ANGLE_COS = 0.172 //80 degrees - } - - fun isNotValidImage(approx: MatOfPoint2f): Boolean { - return isEdgeTouching || isAngleNotCorrect(approx) || isDetectedAreaBelowLimits() - } - - private fun isAngleNotCorrect(approx: MatOfPoint2f): Boolean { - return getMaxCosine(approx) || isLeftEdgeDistorted || isRightEdgeDistorted - } - - private val isRightEdgeDistorted: Boolean - get() = abs(topRightPoint.y - bottomRightPoint.y) > 100 - - private val isLeftEdgeDistorted: Boolean - get() = abs(topLeftPoint.y - bottomLeftPoint.y) > 100 - - private fun getMaxCosine(approx: MatOfPoint2f): Boolean { - var maxCosine = 0.0 - val approxPoints = approx.toArray() - maxCosine = MathUtils.getMaxCosine(maxCosine, approxPoints) - return maxCosine >= SMALLEST_ANGLE_COS - } - - private val isEdgeTouching: Boolean - get() = isTopEdgeTouching || isBottomEdgeTouching || isLeftEdgeTouching || isRightEdgeTouching - - private val isBottomEdgeTouching: Boolean - get() = bottomLeftPoint.x >= previewHeight - 10 || bottomRightPoint.x >= previewHeight - 10 - - private val isTopEdgeTouching: Boolean - get() = topLeftPoint.x <= 10 || topRightPoint.x <= 10 - - private val isRightEdgeTouching: Boolean - get() = topRightPoint.y >= previewWidth - 10 || bottomRightPoint.y >= previewWidth - 10 - - private val isLeftEdgeTouching: Boolean - get() = topLeftPoint.y <= 10 || bottomLeftPoint.y <= 10 - - private fun isDetectedAreaBelowLimits(): Boolean { - return !(previewWidth / previewHeight >= 1 && - resultWidth.toDouble() / resultHeight.toDouble() >= 0.9 && - resultHeight.toDouble() >= 0.70 * previewHeight || - previewHeight / previewWidth >= 1 && - resultHeight.toDouble() / resultWidth.toDouble() >= 0.9 && - resultWidth.toDouble() >= 0.70 * previewWidth) - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/MathUtils.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/MathUtils.kt deleted file mode 100644 index 1c6b1d16d0..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/MathUtils.kt +++ /dev/null @@ -1,51 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.utils - -import org.opencv.core.Point -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.sqrt - -internal object MathUtils { - - private fun angle(p1: Point, p2: Point, p0: Point): Double { - val dx1 = p1.x - p0.x - val dy1 = p1.y - p0.y - val dx2 = p2.x - p0.x - val dy2 = p2.y - p0.y - return (dx1 * dx2 + dy1 * dy2) / sqrt((dx1 * dx1 + dy1 * dy1) * (dx2 * dx2 + dy2 * dy2) + 1e-10) - } - - fun getDistance(p1: Point, p2: Point): Double { - val dx = p2.x - p1.x - val dy = p2.y - p1.y - return sqrt(dx * dx + dy * dy) - } - - fun getMaxCosine(maxCosine: Double, approxPoints: Array): Double { - var newMaxCosine = maxCosine - for (i in 2..4) { - val cosine: Double = abs(angle(approxPoints[i % 4], approxPoints[i - 2], approxPoints[i - 1])) - newMaxCosine = max(cosine, newMaxCosine) - } - return newMaxCosine - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/OpenCvNativeBridge.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/OpenCvNativeBridge.kt deleted file mode 100644 index 0916409902..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/OpenCvNativeBridge.kt +++ /dev/null @@ -1,247 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.utils - -import android.graphics.Bitmap -import android.graphics.PointF -import com.zynksoftware.documentscanner.common.extensions.scaleRectangle -import com.zynksoftware.documentscanner.common.extensions.toBitmap -import com.zynksoftware.documentscanner.common.extensions.toMat -import com.zynksoftware.documentscanner.ui.components.Quadrilateral -import org.opencv.core.* -import org.opencv.imgproc.Imgproc -import java.util.* -import kotlin.collections.ArrayList -import kotlin.math.* - - -internal class OpenCvNativeBridge { - - companion object { - private const val ANGLES_NUMBER = 4 - private const val EPSILON_CONSTANT = 0.02 - private const val CLOSE_KERNEL_SIZE = 10.0 - private const val CANNY_THRESHOLD_LOW = 75.0 - private const val CANNY_THRESHOLD_HIGH = 200.0 - private const val CUTOFF_THRESHOLD = 155.0 - private const val TRUNCATE_THRESHOLD = 150.0 - private const val NORMALIZATION_MIN_VALUE = 0.0 - private const val NORMALIZATION_MAX_VALUE = 255.0 - private const val BLURRING_KERNEL_SIZE = 5.0 - private const val DOWNSCALE_IMAGE_SIZE = 600.0 - private const val FIRST_MAX_CONTOURS = 10 - } - - fun getScannedBitmap(bitmap: Bitmap, x1: Float, y1: Float, x2: Float, y2: Float, x3: Float, y3: Float, x4: Float, y4: Float): Bitmap { - val rectangle = MatOfPoint2f() - rectangle.fromArray( - Point(x1.toDouble(), y1.toDouble()), - Point(x2.toDouble(), y2.toDouble()), - Point(x3.toDouble(), y3.toDouble()), - Point(x4.toDouble(), y4.toDouble()) - ) - val dstMat = PerspectiveTransformation.transform(bitmap.toMat(), rectangle) - return dstMat.toBitmap() - } - - fun getContourEdgePoints(tempBitmap: Bitmap): List { - var point2f = getPoint(tempBitmap) - if (point2f == null) point2f = MatOfPoint2f() - val points: List = point2f.toArray().toList() - val result: MutableList = ArrayList() - for (i in points.indices) { - result.add(PointF(points[i].x.toFloat(), points[i].y.toFloat())) - } - - return result - } - - fun getPoint(bitmap: Bitmap): MatOfPoint2f? { - val src = bitmap.toMat() - - val ratio = DOWNSCALE_IMAGE_SIZE / max(src.width(), src.height()) - val downscaledSize = Size(src.width() * ratio, src.height() * ratio) - val downscaled = Mat(downscaledSize, src.type()) - Imgproc.resize(src, downscaled, downscaledSize) - val largestRectangle = detectLargestQuadrilateral(downscaled) - - return largestRectangle?.contour?.scaleRectangle(1f / ratio) - } - - // patch from Udayraj123 (https://github.com/Udayraj123/LiveEdgeDetection) - fun detectLargestQuadrilateral(src: Mat): Quadrilateral? { - val destination = Mat() - Imgproc.blur(src, src, Size(BLURRING_KERNEL_SIZE, BLURRING_KERNEL_SIZE)) - - Core.normalize(src, src, NORMALIZATION_MIN_VALUE, NORMALIZATION_MAX_VALUE, Core.NORM_MINMAX) - - Imgproc.threshold(src, src, TRUNCATE_THRESHOLD, NORMALIZATION_MAX_VALUE, Imgproc.THRESH_TRUNC) - Core.normalize(src, src, NORMALIZATION_MIN_VALUE, NORMALIZATION_MAX_VALUE, Core.NORM_MINMAX) - - Imgproc.Canny(src, destination, CANNY_THRESHOLD_HIGH, CANNY_THRESHOLD_LOW) - - Imgproc.threshold(destination, destination, CUTOFF_THRESHOLD, NORMALIZATION_MAX_VALUE, Imgproc.THRESH_TOZERO) - - Imgproc.morphologyEx( - destination, destination, Imgproc.MORPH_CLOSE, - Mat(Size(CLOSE_KERNEL_SIZE, CLOSE_KERNEL_SIZE), CvType.CV_8UC1, Scalar(NORMALIZATION_MAX_VALUE)), - Point(-1.0, -1.0), 1 - ) - - val largestContour: List? = findLargestContours(destination) - if (null != largestContour) { - return findQuadrilateral(largestContour) - } - return null - } - - private fun findQuadrilateral(mContourList: List): Quadrilateral? { - for (c in mContourList) { - val c2f = MatOfPoint2f(*c.toArray()) - val peri = Imgproc.arcLength(c2f, true) - val approx = MatOfPoint2f() - Imgproc.approxPolyDP(c2f, approx, EPSILON_CONSTANT * peri, true) - val points = approx.toArray() - // select biggest 4 angles polygon - if (approx.rows() == ANGLES_NUMBER) { - val foundPoints: Array = sortPoints(points) - return Quadrilateral(approx, foundPoints) - } else if(approx.rows() == 5) { - // if document has a bent corner - var shortestDistance = Int.MAX_VALUE.toDouble() - var shortestPoint1: Point? = null - var shortestPoint2: Point? = null - - var diagonal = 0.toDouble() - var diagonalPoint1: Point? = null - var diagonalPoint2: Point? = null - - for (i in 0 until 4) { - for (j in i + 1 until 5) { - val d = distance(points[i], points[j]) - if (d < shortestDistance) { - shortestDistance = d - shortestPoint1 = points[i] - shortestPoint2 = points[j] - } - if(d > diagonal) { - diagonal = d - diagonalPoint1 = points[i] - diagonalPoint2 = points[j] - } - } - } - - val trianglePointWithHypotenuse: Point? = points.toList().minus(arrayListOf(shortestPoint1, shortestPoint2, diagonalPoint1, diagonalPoint2))[0] - - val newPoint = if(trianglePointWithHypotenuse!!.x > shortestPoint1!!.x && trianglePointWithHypotenuse.x > shortestPoint2!!.x && - trianglePointWithHypotenuse.y > shortestPoint1.y && trianglePointWithHypotenuse.y > shortestPoint2.y) { - Point(min(shortestPoint1.x, shortestPoint2.x), min(shortestPoint1.y, shortestPoint2.y)) - } else if(trianglePointWithHypotenuse.x < shortestPoint1.x && trianglePointWithHypotenuse.x < shortestPoint2!!.x && - trianglePointWithHypotenuse.y > shortestPoint1.y && trianglePointWithHypotenuse.y > shortestPoint2.y) { - Point(max(shortestPoint1.x, shortestPoint2.x), min(shortestPoint1.y, shortestPoint2.y)) - } else if(trianglePointWithHypotenuse.x < shortestPoint1.x && trianglePointWithHypotenuse.x < shortestPoint2!!.x && - trianglePointWithHypotenuse.y < shortestPoint1.y && trianglePointWithHypotenuse.y < shortestPoint2.y) { - Point(max(shortestPoint1.x, shortestPoint2.x), max(shortestPoint1.y, shortestPoint2.y)) - } else if(trianglePointWithHypotenuse.x > shortestPoint1.x && trianglePointWithHypotenuse.x > shortestPoint2!!.x && - trianglePointWithHypotenuse.y < shortestPoint1.y && trianglePointWithHypotenuse.y < shortestPoint2.y) { - Point(min(shortestPoint1.x, shortestPoint2.x), max(shortestPoint1.y, shortestPoint2.y)) - } else { - Point(0.0, 0.0) - } - - val sortedPoints = sortPoints(arrayOf(trianglePointWithHypotenuse, diagonalPoint1!!, diagonalPoint2!!, newPoint)) - val newApprox = MatOfPoint2f() - newApprox.fromArray(*sortedPoints) - return Quadrilateral(newApprox, sortedPoints) - } - } - return null - } - - private fun distance(p1: Point, p2: Point): Double { - return sqrt((p1.x - p2.x).pow(2.0) + (p1.y - p2.y).pow(2.0)) - } - - private fun sortPoints(src: Array): Array { - val srcPoints: ArrayList = ArrayList(src.toList()) - val result = arrayOf(null, null, null, null) - val sumComparator: Comparator = Comparator { lhs, rhs -> (lhs.y + lhs.x).compareTo(rhs.y + rhs.x) } - val diffComparator: Comparator = Comparator { lhs, rhs -> (lhs.y - lhs.x).compareTo(rhs.y - rhs.x) } - - // top-left corner = minimal sum - result[0] = Collections.min(srcPoints, sumComparator) - // bottom-right corner = maximal sum - result[2] = Collections.max(srcPoints, sumComparator) - // top-right corner = minimal difference - result[1] = Collections.min(srcPoints, diffComparator) - // bottom-left corner = maximal difference - result[3] = Collections.max(srcPoints, diffComparator) - return result.map { - it!! - }.toTypedArray() - } - - private fun findLargestContours(inputMat: Mat): List? { - val mHierarchy = Mat() - val mContourList: List = ArrayList() - //finding contours - as we are sorting by area anyway, we can use RETR_LIST - faster than RETR_EXTERNAL. - Imgproc.findContours(inputMat, mContourList, mHierarchy, Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE) - - // Convert the contours to their Convex Hulls i.e. removes minor nuances in the contour - val mHullList: MutableList = ArrayList() - val tempHullIndices = MatOfInt() - for (i in mContourList.indices) { - Imgproc.convexHull(mContourList[i], tempHullIndices) - mHullList.add(hull2Points(tempHullIndices, mContourList[i])) - } - // Release mContourList as its job is done - for (c in mContourList) { - c.release() - } - tempHullIndices.release() - mHierarchy.release() - if (mHullList.size != 0) { - mHullList.sortWith { lhs, rhs -> - Imgproc.contourArea(rhs).compareTo(Imgproc.contourArea(lhs)) - } - return mHullList.subList(0, min(mHullList.size, FIRST_MAX_CONTOURS)) - } - return null - } - - private fun hull2Points(hull: MatOfInt, contour: MatOfPoint): MatOfPoint { - val indexes = hull.toList() - val points: MutableList = ArrayList() - val ctrList = contour.toList() - for (index in indexes) { - points.add(ctrList[index]) - } - val point = MatOfPoint() - point.fromList(points) - return point - } - - fun contourArea(approx: MatOfPoint2f): Double { - return Imgproc.contourArea(approx) - } - - -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/PerspectiveTransformation.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/PerspectiveTransformation.kt deleted file mode 100644 index d5a077b8f9..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/common/utils/PerspectiveTransformation.kt +++ /dev/null @@ -1,117 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.common.utils - -import com.zynksoftware.documentscanner.common.utils.MathUtils.getDistance -import org.opencv.core.Mat -import org.opencv.core.MatOfPoint2f -import org.opencv.core.Point -import org.opencv.core.Size -import org.opencv.imgproc.Imgproc -import java.util.* - -internal object PerspectiveTransformation { - - fun transform(src: Mat, corners: MatOfPoint2f): Mat { - val sortedCorners = sortCorners(corners) - val size = getRectangleSize(sortedCorners) - val result = Mat.zeros(size, src.type()) - val imageOutline = getOutline(result) - val transformation = Imgproc.getPerspectiveTransform(sortedCorners, imageOutline) - Imgproc.warpPerspective(src, result, transformation, size) - return result - } - - private fun getRectangleSize(rectangle: MatOfPoint2f): Size { - val corners = rectangle.toArray() - val top = getDistance(corners[0], corners[1]) - val right = getDistance(corners[1], corners[2]) - val bottom = getDistance(corners[2], corners[3]) - val left = getDistance(corners[3], corners[0]) - val averageWidth = (top + bottom) / 2f - val averageHeight = (right + left) / 2f - return Size(Point(averageWidth, averageHeight)) - } - - private fun getOutline(image: Mat): MatOfPoint2f { - val topLeft = Point(0.toDouble(), 0.toDouble()) - val topRight = Point(image.cols().toDouble(), 0.toDouble()) - val bottomRight = Point(image.cols().toDouble(), image.rows().toDouble()) - val bottomLeft = Point(0.toDouble(), image.rows().toDouble()) - val points = arrayOf(topLeft, topRight, bottomRight, bottomLeft) - val result = MatOfPoint2f() - result.fromArray(*points) - return result - } - - private fun sortCorners(corners: MatOfPoint2f): MatOfPoint2f { - val center = getMassCenter(corners) - val points = corners.toList() - val topPoints: MutableList = ArrayList() - val bottomPoints: MutableList = ArrayList() - for (point in points) { - if (point.y < center.y) { - topPoints.add(point) - } else { - bottomPoints.add(point) - } - } - - val topLeft = if (topPoints[0].x > topPoints[1].x) { - topPoints[1] - } else { - topPoints[0] - } - - val topRight = if (topPoints[0].x > topPoints[1].x) { - topPoints[0] - } else { - topPoints[1] - } - - val bottomLeft = if (bottomPoints[0].x > bottomPoints[1].x) { - bottomPoints[1] - } else { - bottomPoints[0] - } - - val bottomRight = if (bottomPoints[0].x > bottomPoints[1].x) { - bottomPoints[0] - } else { - bottomPoints[1] - } - val result = MatOfPoint2f() - val sortedPoints = arrayOf(topLeft, topRight, bottomRight, bottomLeft) - result.fromArray(*sortedPoints) - return result - } - - private fun getMassCenter(points: MatOfPoint2f): Point { - var xSum = 0.0 - var ySum = 0.0 - val pointList = points.toList() - val len = pointList.size - for (point in pointList) { - xSum += point.x - ySum += point.y - } - return Point(xSum / len, ySum / len) - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/manager/SessionManager.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/manager/SessionManager.kt deleted file mode 100644 index b1d786476a..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/manager/SessionManager.kt +++ /dev/null @@ -1,68 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.manager - -import android.content.Context -import android.graphics.Bitmap -import id.zelory.compressor.extension -import java.util.Locale - -internal class SessionManager(context: Context) { - - companion object { - private const val IMAGE_SIZE_KEY = "IMAGE_SIZE_KEY" - private const val IMAGE_QUALITY_KEY = "IMAGE_QUALITY_KEY" - private const val IMAGE_TYPE_KEY = "IMAGE_TYPE_KEY" - - private const val DEFAULT_IMAGE_TYPE = "jpg" - } - private val preferences = context.getSharedPreferences("ZDC_Shared_Preferences", Context.MODE_PRIVATE) - - - fun getImageSize(): Long { - return preferences.getLong(IMAGE_SIZE_KEY, -1L) - } - - fun setImageSize(size: Long) { - preferences.edit().putLong(IMAGE_SIZE_KEY, size).apply() - } - - fun getImageQuality(): Int { - return preferences.getInt(IMAGE_QUALITY_KEY, 100) - } - - fun setImageQuality(quality: Int) { - preferences.edit().putInt(IMAGE_QUALITY_KEY, quality).apply() - } - - fun getImageType(): Bitmap.CompressFormat { - return compressFormat(preferences.getString(IMAGE_TYPE_KEY, DEFAULT_IMAGE_TYPE)!!) - } - - fun setImageType(type: Bitmap.CompressFormat) { - preferences.edit().putString(IMAGE_TYPE_KEY, type.extension()).apply() - } - - private fun compressFormat(format: String) = when (format.lowercase(Locale.getDefault())) { - "png" -> Bitmap.CompressFormat.PNG - "webp" -> Bitmap.CompressFormat.WEBP - else -> Bitmap.CompressFormat.JPEG - } -} diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/model/DocumentScannerErrorModel.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/model/DocumentScannerErrorModel.kt deleted file mode 100644 index 1cc0f9d99a..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/model/DocumentScannerErrorModel.kt +++ /dev/null @@ -1,38 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.model - -data class DocumentScannerErrorModel( - var errorMessage: ErrorMessage? = null, - var throwable: Throwable? = null -) { - enum class ErrorMessage(val error: String){ - TAKE_IMAGE_FROM_GALLERY_ERROR("TAKE_IMAGE_FROM_GALLERY_ERROR"), - PHOTO_CAPTURE_FAILED("PHOTO_CAPTURE_FAILED"), - CAMERA_USE_CASE_BINDING_FAILED("CAMERA_USE_CASE_BINDING_FAILED"), - DETECT_LARGEST_QUADRILATERAL_FAILED("DETECT_LARGEST_QUADRILATERAL_FAILED"), - INVALID_IMAGE("INVALID_IMAGE"), - CAMERA_PERMISSION_REFUSED_WITHOUT_NEVER_ASK_AGAIN("CAMERA_PERMISSION_REFUSED_WITHOUT_NEVER_ASK_AGAIN"), - CAMERA_PERMISSION_REFUSED_GO_TO_SETTINGS("CAMERA_PERMISSION_REFUSED_GO_TO_SETTINGS"), - STORAGE_PERMISSION_REFUSED_WITHOUT_NEVER_ASK_AGAIN("STORAGE_PERMISSION_REFUSED_WITHOUT_NEVER_ASK_AGAIN"), - STORAGE_PERMISSION_REFUSED_GO_TO_SETTINGS("STORAGE_PERMISSION_REFUSED_GO_TO_SETTINGS"), - CROPPING_FAILED("CROPPING_FAILED"); - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/model/ScannerResults.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/model/ScannerResults.kt deleted file mode 100644 index 6bdf126742..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/model/ScannerResults.kt +++ /dev/null @@ -1,28 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.model - -import java.io.File - -data class ScannerResults ( - val originalImageFile: File? = null, - val croppedImageFile: File? = null, - val transformedImageFile: File? = null -) \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/DocumentScanner.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/DocumentScanner.kt deleted file mode 100644 index c335a5093c..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/DocumentScanner.kt +++ /dev/null @@ -1,46 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui - -import android.content.Context -import android.graphics.Bitmap -import com.zynksoftware.documentscanner.manager.SessionManager - -object DocumentScanner { - - fun init(context: Context, configuration: Configuration = Configuration()) { - System.loadLibrary("opencv_java4") - val sessionManager = SessionManager(context) - if(configuration.imageQuality in 1..100) { - sessionManager.setImageQuality(configuration.imageQuality) - } - sessionManager.setImageSize(configuration.imageSize) - sessionManager.setImageType(configuration.imageType) - } - - - data class Configuration( - var imageQuality: Int = 100, - var imageSize: Long = -1, - var imageType: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG - ){ - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/base/BaseFragment.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/base/BaseFragment.kt deleted file mode 100644 index 0984bb5311..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/base/BaseFragment.kt +++ /dev/null @@ -1,58 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.ui.base - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.RelativeLayout -import androidx.fragment.app.Fragment -import androidx.viewbinding.ViewBinding -import com.zynksoftware.documentscanner.R -import com.zynksoftware.documentscanner.common.extensions.hide -import com.zynksoftware.documentscanner.common.extensions.show - -internal abstract class BaseFragment(private val bindingInflater: (layoutInflater: LayoutInflater) -> BINDING) : Fragment() { - - private var _binding: BINDING? = null - - // This property is only valid between onCreateView and onDestroyView. - val binding get() = _binding!! - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - _binding = bindingInflater(inflater) - return binding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - fun showProgressBar() { - view?.findViewById(R.id.progressLayout)?.show() - } - - fun hideProgressBar() { - view?.findViewById(R.id.progressLayout)?.hide() - } - -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/camerascreen/CameraScreenFragment.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/camerascreen/CameraScreenFragment.kt deleted file mode 100644 index 903a80640c..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/camerascreen/CameraScreenFragment.kt +++ /dev/null @@ -1,213 +0,0 @@ -/** - Copyright 2020 ZynkSoftware SRL - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, publish, distribute, - sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.ui.camerascreen - -import android.Manifest -import android.net.Uri -import android.os.Bundle -import android.util.Log -import android.view.View -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat -import com.zynksoftware.documentscanner.R -import com.zynksoftware.documentscanner.common.extensions.hide -import com.zynksoftware.documentscanner.common.extensions.show -import com.zynksoftware.documentscanner.common.utils.FileUriUtils -import com.zynksoftware.documentscanner.databinding.FragmentCameraScreenBinding -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel -import com.zynksoftware.documentscanner.ui.base.BaseFragment -import com.zynksoftware.documentscanner.ui.components.scansurface.ScanSurfaceListener -import com.zynksoftware.documentscanner.ui.scan.InternalScanActivity -import java.io.File -import java.io.FileNotFoundException - - -internal class CameraScreenFragment: BaseFragment(FragmentCameraScreenBinding::inflate), ScanSurfaceListener { - - private val requestCameraPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> - if (isGranted) { - startCamera() - } else { - onError(DocumentScannerErrorModel(DocumentScannerErrorModel.ErrorMessage.CAMERA_PERMISSION_REFUSED_GO_TO_SETTINGS)) - } - } - - private val filePickerContract = registerForActivityResult(ActivityResultContracts.GetContent()) { - handleSelectedFile(it) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) { - super.onViewCreated(view, savedInstanceState) - - scanSurfaceView.lifecycleOwner = this@CameraScreenFragment - scanSurfaceView.listener = this@CameraScreenFragment - scanSurfaceView.originalImageFile = getScanActivity().originalImageFile - - checkForCameraPermissions() - initListeners() - } - - override fun onDestroy() { - super.onDestroy() - if(getScanActivity().shouldCallOnClose) { - getScanActivity().onClose() - } - } - - override fun onResume() { - super.onResume() - getScanActivity().reInitOriginalImageFile() - binding.scanSurfaceView.originalImageFile = getScanActivity().originalImageFile - } - - private fun initListeners() = with(binding) { - cameraCaptureButton.setOnClickListener { - takePhoto() - } - cancelButton.setOnClickListener { - finishActivity() - } - flashButton.setOnClickListener { - switchFlashState() - } - galleryButton.setOnClickListener { - selectImageFromGallery() - } - autoButton.setOnClickListener { - toggleAutoManualButton() - } - } - - private fun toggleAutoManualButton() = with(binding) { - scanSurfaceView.isAutoCaptureOn = !scanSurfaceView.isAutoCaptureOn - if (scanSurfaceView.isAutoCaptureOn) { - autoButton.text = getString(R.string.zdc_auto) - } else { - autoButton.text = getString(R.string.zdc_manual) - } - } - - private fun checkForCameraPermissions() { - ContextCompat.checkSelfPermission(requireActivity(), Manifest.permission.CAMERA).let { - if (it == android.content.pm.PackageManager.PERMISSION_GRANTED) { - startCamera() - } else { - requestCameraPermission.launch(Manifest.permission.CAMERA) - } - } - } - - private fun startCamera() { - binding.scanSurfaceView.start() - } - - private fun takePhoto() { - binding.scanSurfaceView.takePicture() - } - - private fun getScanActivity(): InternalScanActivity { - return (requireActivity() as InternalScanActivity) - } - - private fun finishActivity() { - getScanActivity().finish() - } - - private fun switchFlashState() { - binding.scanSurfaceView.switchFlashState() - } - - override fun showFlash() { - binding.flashButton.show() - } - - override fun hideFlash() { - binding.flashButton.hide() - } - - private fun selectImageFromGallery() { - filePickerContract.launch("image/*") - } - - private fun handleSelectedFile(imageUri: Uri?) { - try { - if (imageUri != null) { - val realPath = FileUriUtils.getRealPath(getScanActivity(), imageUri) - if (realPath != null) { - getScanActivity().reInitOriginalImageFile() - getScanActivity().originalImageFile = File(realPath) - startCroppingProcess() - } else { - Log.e(TAG, DocumentScannerErrorModel.ErrorMessage.TAKE_IMAGE_FROM_GALLERY_ERROR.error) - onError(DocumentScannerErrorModel( - DocumentScannerErrorModel.ErrorMessage.TAKE_IMAGE_FROM_GALLERY_ERROR, null)) - } - } else { - Log.e(TAG, DocumentScannerErrorModel.ErrorMessage.TAKE_IMAGE_FROM_GALLERY_ERROR.error) - onError(DocumentScannerErrorModel( - DocumentScannerErrorModel.ErrorMessage.TAKE_IMAGE_FROM_GALLERY_ERROR, null)) - } - } catch (e: FileNotFoundException) { - Log.e(TAG, "FileNotFoundException", e) - onError(DocumentScannerErrorModel( - DocumentScannerErrorModel.ErrorMessage.TAKE_IMAGE_FROM_GALLERY_ERROR, e)) - } - } - - override fun scanSurfacePictureTaken() { - startCroppingProcess() - } - - private fun startCroppingProcess() { - if (isAdded) { - getScanActivity().showImageCropFragment() - } - } - - override fun scanSurfaceShowProgress() { - showProgressBar() - } - - override fun scanSurfaceHideProgress() { - hideProgressBar() - } - - override fun onError(error: DocumentScannerErrorModel) { - if(isAdded) { - getScanActivity().onError(error) - } - } - - override fun showFlashModeOn() { - binding.flashButton.setImageResource(R.drawable.zdc_flash_on) - } - - override fun showFlashModeOff() { - binding.flashButton.setImageResource(R.drawable.zdc_flash_off) - } - - companion object { - private val TAG = CameraScreenFragment::class.simpleName - - fun newInstance(): CameraScreenFragment { - return CameraScreenFragment() - } - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/ProgressView.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/ProgressView.kt deleted file mode 100644 index f8ab5787e8..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/ProgressView.kt +++ /dev/null @@ -1,38 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.components - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.widget.RelativeLayout -import com.zynksoftware.documentscanner.R - -internal class ProgressView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : RelativeLayout(context, attrs, defStyleAttr) { - - init { - LayoutInflater.from(context).inflate(R.layout.progress_layout, this, true) - } -} diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/Quadrilateral.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/Quadrilateral.kt deleted file mode 100644 index e00482089e..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/Quadrilateral.kt +++ /dev/null @@ -1,26 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.components - -import org.opencv.core.MatOfPoint2f -import org.opencv.core.Point - -internal class Quadrilateral(val contour: MatOfPoint2f, val points: Array) \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/ScanCanvasView.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/ScanCanvasView.kt deleted file mode 100644 index af550b984b..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/ScanCanvasView.kt +++ /dev/null @@ -1,183 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.components - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.Path -import android.os.Handler -import android.os.Looper -import android.util.AttributeSet -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import androidx.core.content.ContextCompat -import com.zynksoftware.documentscanner.R -import org.opencv.core.Point - -internal class ScanCanvasView : FrameLayout { - - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) - - companion object { - private const val CLEAR_SHAPE_DELAY_IN_MILLIS = 600L - private const val POINTER_ANIMATION_DURATION = 300L - } - - private var paint = Paint() - private var border = Paint() - private val handlerClear = Handler(Looper.getMainLooper()) - - private var shouldAnimate = true - - var pointer1: View = View(context) - var pointer2: View = View(context) - var pointer3: View = View(context) - var pointer4: View = View(context) - - init { - paint.color = ContextCompat.getColor(context, R.color.zdc_white_transparent) - border.color = ContextCompat.getColor(context, android.R.color.white) - border.strokeWidth = context.resources.getDimension(R.dimen.zdc_polygon_line_stroke_width) - border.style = Paint.Style.STROKE - border.isAntiAlias = true - paint.isAntiAlias = true - - pointer1.layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) - pointer2.layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) - pointer3.layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) - pointer4.layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) - - clearPointersPosition() - - addView(pointer1) - addView(pointer2) - addView(pointer3) - addView(pointer4) - } - - private fun clearPointersPosition() { - pointer1.x = 0F - pointer1.y = 0F - pointer2.x = 0F - pointer2.y = 0F - pointer3.x = 0F - pointer3.y = 0F - pointer4.x = 0F - pointer4.y = 0F - } - - override fun dispatchDraw(canvas: Canvas) { - super.dispatchDraw(canvas) - - previewWidth?.let { previewWidth -> - previewHeight?.let { previewHeight -> - canvas.scale(width / previewWidth, height / previewHeight) - } - } - - canvas.drawLine(pointer1.x, pointer1.y, pointer4.x, pointer4.y, border) - canvas.drawLine(pointer1.x, pointer1.y, pointer2.x, pointer2.y, border) - canvas.drawLine(pointer3.x, pointer3.y, pointer4.x, pointer4.y, border) - canvas.drawLine(pointer2.x, pointer2.y, pointer3.x, pointer3.y, border) - - val path = Path() - path.moveTo(pointer1.x, pointer1.y) - path.lineTo(pointer2.x, pointer2.y) - path.lineTo(pointer3.x, pointer3.y) - path.lineTo(pointer4.x, pointer4.y) - path.close() - - path.let { - canvas.drawPath(it, paint) - } - } - - var previewWidth: Float? = null - var previewHeight: Float? = null - - fun showShape(previewWidth: Float, previewHeight: Float, points: Array) { - this.previewWidth = previewWidth - this.previewHeight = previewHeight - - val pointer1x = previewWidth - points[0].y.toFloat() - val pointer1y = points[0].x.toFloat() - val pointer2x = previewWidth - points[1].y.toFloat() - val pointer2y = points[1].x.toFloat() - val pointer3x = previewWidth - points[2].y.toFloat() - val pointer3y = points[2].x.toFloat() - val pointer4x = previewWidth - points[3].y.toFloat() - val pointer4y = points[3].x.toFloat() - - if (pointer1.x == 0F && pointer1.y == 0F) { - pointer1.x = pointer1x - pointer1.y = pointer1y - pointer2.x = pointer2x - pointer2.y = pointer2y - pointer3.x = pointer3x - pointer3.y = pointer3y - pointer4.x = pointer4x - pointer4.y = pointer4y - } else { - if (shouldAnimate) { - shouldAnimate = false - - pointer1.animate().translationX(pointer1x).translationY(pointer1y) - .setDuration(POINTER_ANIMATION_DURATION).withEndAction { - shouldAnimate = true - }.start() - - pointer2.animate().translationX(pointer2x).translationY(pointer2y) - .setDuration(POINTER_ANIMATION_DURATION).withEndAction { - shouldAnimate = true - }.start() - - pointer3.animate().translationX(pointer3x).translationY(pointer3y) - .setDuration(POINTER_ANIMATION_DURATION).withEndAction { - shouldAnimate = true - }.start() - - pointer4.animate().translationX(pointer4x).translationY(pointer4y) - .setDuration(POINTER_ANIMATION_DURATION).withEndAction { - shouldAnimate = true - }.start() - } - } - - handlerClear.removeCallbacks(runnable) - invalidate() - } - - fun clearShape() { - handlerClear.postDelayed(runnable, CLEAR_SHAPE_DELAY_IN_MILLIS) - } - - private val runnable = Runnable { - pointer1.clearAnimation() - pointer2.clearAnimation() - pointer3.clearAnimation() - pointer4.clearAnimation() - clearPointersPosition() - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/polygon/PolygonPointImageView.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/polygon/PolygonPointImageView.kt deleted file mode 100644 index 15b82ece1c..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/polygon/PolygonPointImageView.kt +++ /dev/null @@ -1,85 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package com.zynksoftware.documentscanner.ui.components.polygon - -import android.content.Context -import android.graphics.PointF -import android.util.AttributeSet -import android.view.MotionEvent -import androidx.appcompat.widget.AppCompatImageView -import androidx.core.content.ContextCompat -import com.zynksoftware.documentscanner.R - -internal class PolygonPointImageView @JvmOverloads constructor( - context: Context, - private val polygonView: PolygonView? = null, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : AppCompatImageView(context, attrs, defStyleAttr) { - - private var downPoint = PointF() - private var startPoint = PointF() - - override fun onTouchEvent(event: MotionEvent): Boolean { - super.onTouchEvent(event) - - if (polygonView != null) { - when (event.action) { - MotionEvent.ACTION_MOVE -> { - val mv = PointF(event.x - downPoint.x, event.y - downPoint.y) - if (startPoint.x + mv.x + width < polygonView.width && - startPoint.y + mv.y + height < polygonView.height && - startPoint.x + mv.x > 0 && startPoint.y + mv.y > 0 - ) { - x = startPoint.x + mv.x - y = startPoint.y + mv.y - startPoint = PointF(x, y) - } - } - MotionEvent.ACTION_DOWN -> { - downPoint.x = event.x - downPoint.y = event.y - startPoint = PointF(x, y) - } - MotionEvent.ACTION_UP -> { - performClick() - } - } - polygonView.invalidate() - } - return true - } - - // Because we call this from onTouchEvent, this code will be executed for both - // normal touch events and for when the system calls this using Accessibility - override fun performClick(): Boolean { - super.performClick() - - val color = if (polygonView?.isValidShape(polygonView.getPoints()) == true) { - ContextCompat.getColor(context, android.R.color.white) - } else { - ContextCompat.getColor(context, R.color.zdc_red) - } - polygonView?.paint?.color = color - - return true - } - -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/polygon/PolygonView.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/polygon/PolygonView.kt deleted file mode 100644 index 001f697f5d..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/polygon/PolygonView.kt +++ /dev/null @@ -1,183 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.components.polygon - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.PointF -import android.util.AttributeSet -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.ImageView -import androidx.core.content.ContextCompat -import com.zynksoftware.documentscanner.R -import java.util.* - -internal class PolygonView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : FrameLayout(context, attrs, defStyleAttr) { - - var paint: Paint = Paint() - private var pointer1: ImageView - private var pointer2: ImageView - private var pointer3: ImageView - private var pointer4: ImageView - private var pointPadding = resources.getDimension(R.dimen.zdc_point_padding).toInt() - - companion object { - private val TAG = PolygonView::class.simpleName - private const val HALF = 2 - private const val THREE_PARTS = 3 - } - - init { - pointer1 = getImageView(0, 0) - pointer2 = getImageView(width, 0) - pointer3 = getImageView(0, height) - pointer4 = getImageView(width, height) - - addView(pointer1) - addView(pointer2) - addView(pointer3) - addView(pointer4) - - paint.color = ContextCompat.getColor(context, android.R.color.white) - paint.strokeWidth = context.resources.getDimension(R.dimen.zdc_polygon_line_stroke_width) - paint.isAntiAlias = true - } - - fun getOrderedValidEdgePoints(tempBitmap: Bitmap, pointFs: List): Map { - var orderedPoints: Map = getOrderedPoints(pointFs) - if (!isValidShape(orderedPoints)) { - orderedPoints = getOutlinePoints(tempBitmap) - } - return orderedPoints - } - - fun setPoints(pointFMap: Map) { - if (pointFMap.size == 4) { - setPointsCoordinates(pointFMap) - } - } - - fun getPoints(): Map { - val points: MutableList = ArrayList() - points.add(PointF(pointer1.x, pointer1.y)) - points.add(PointF(pointer2.x, pointer2.y)) - points.add(PointF(pointer3.x, pointer3.y)) - points.add(PointF(pointer4.x, pointer4.y)) - return getOrderedPoints(points) - } - - fun isValidShape(pointFMap: Map): Boolean { - return pointFMap.size == 4 - } - - private fun getOutlinePoints(tempBitmap: Bitmap): Map { - val offsetWidth = (tempBitmap.width / THREE_PARTS).toFloat() - val offsetHeight = (tempBitmap.height / THREE_PARTS).toFloat() - val screenXCenter = tempBitmap.width / HALF - val screenYCenter = tempBitmap.height / HALF - val outlinePoints: MutableMap = HashMap() - outlinePoints[0] = PointF(screenXCenter - offsetWidth, screenYCenter - offsetHeight) - outlinePoints[1] = PointF(screenXCenter + offsetWidth, screenYCenter - offsetHeight) - outlinePoints[2] = PointF(screenXCenter - offsetWidth , screenYCenter + offsetHeight) - outlinePoints[3] = PointF(screenXCenter + offsetWidth, screenYCenter + offsetHeight) - return outlinePoints - } - - private fun getOrderedPoints(points: List): Map { - val centerPoint = PointF() - val size = points.size - for (pointF in points) { - centerPoint.x += pointF.x / size - centerPoint.y += pointF.y / size - } - val orderedPoints: MutableMap = HashMap() - for (pointF in points) { - var index = -1 - if (pointF.x < centerPoint.x && pointF.y < centerPoint.y) { - index = 0 - } else if (pointF.x > centerPoint.x && pointF.y < centerPoint.y) { - index = 1 - } else if (pointF.x < centerPoint.x && pointF.y > centerPoint.y) { - index = 2 - } else if (pointF.x > centerPoint.x && pointF.y > centerPoint.y) { - index = 3 - } - orderedPoints[index] = pointF - } - return orderedPoints - } - - private fun setPointsCoordinates(pointFMap: Map) { - pointer1.x = pointFMap.getValue(0).x - pointPadding - pointer1.y = pointFMap.getValue(0).y - pointPadding - - pointer2.x = pointFMap.getValue(1).x - pointPadding - pointer2.y = pointFMap.getValue(1).y - pointPadding - - pointer3.x = pointFMap.getValue(2).x - pointPadding - pointer3.y = pointFMap.getValue(2).y - pointPadding - - pointer4.x = pointFMap.getValue(3).x - pointPadding - pointer4.y = pointFMap.getValue(3).y - pointPadding - } - - override fun dispatchDraw(canvas: Canvas) { - super.dispatchDraw(canvas) - canvas.drawLine( - pointer1.x + pointer1.width / 2, pointer1.y + pointer1.height / 2, - pointer3.x + pointer3.width / 2, pointer3.y + pointer3.height / 2, - paint - ) - canvas.drawLine( - pointer1.x + pointer1.width / 2, pointer1.y + pointer1.height / 2, - pointer2.x + pointer2.width / 2, pointer2.y + pointer2.height / 2, - paint - ) - canvas.drawLine( - pointer2.x + pointer2.width / 2, pointer2.y + pointer2.height / 2, - pointer4.x + pointer4.width / 2, pointer4.y + pointer4.height / 2, - paint - ) - canvas.drawLine( - pointer3.x + pointer3.width / 2, pointer3.y + pointer3.height / 2, - pointer4.x + pointer4.width / 2, pointer4.y + pointer4.height / 2, - paint - ) - } - - private fun getImageView(x: Int, y: Int): ImageView { - val imageView = PolygonPointImageView(context, this) - val layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) - imageView.layoutParams = layoutParams - imageView.setImageResource(R.drawable.crop_corner_circle) - imageView.setPadding(pointPadding, pointPadding, pointPadding, pointPadding) - imageView.x = x.toFloat() - imageView.y = y.toFloat() - return imageView - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/scansurface/ScanSurfaceListener.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/scansurface/ScanSurfaceListener.kt deleted file mode 100644 index 0c742f817b..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/scansurface/ScanSurfaceListener.kt +++ /dev/null @@ -1,35 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.components.scansurface - -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel - -internal interface ScanSurfaceListener { - fun scanSurfacePictureTaken() - fun scanSurfaceShowProgress() - fun scanSurfaceHideProgress() - fun onError(error: DocumentScannerErrorModel) - - fun showFlash() - fun hideFlash() - fun showFlashModeOn() - fun showFlashModeOff() -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/scansurface/ScanSurfaceView.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/scansurface/ScanSurfaceView.kt deleted file mode 100755 index 88e14f42fb..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/components/scansurface/ScanSurfaceView.kt +++ /dev/null @@ -1,289 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.components.scansurface - -import android.content.Context -import android.os.CountDownTimer -import android.util.AttributeSet -import android.util.Log -import android.view.LayoutInflater -import android.view.Surface -import android.widget.FrameLayout -import androidx.camera.core.* -import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.core.content.ContextCompat -import androidx.lifecycle.LifecycleOwner -import com.zynksoftware.documentscanner.R -import com.zynksoftware.documentscanner.common.extensions.yuvToRgba -import com.zynksoftware.documentscanner.common.utils.ImageDetectionProperties -import com.zynksoftware.documentscanner.common.utils.OpenCvNativeBridge -import com.zynksoftware.documentscanner.databinding.ScanSurfaceViewBinding -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel.ErrorMessage -import org.opencv.core.MatOfPoint2f -import org.opencv.core.Point -import org.opencv.core.Size -import java.io.File -import kotlin.math.max -import kotlin.math.min -import kotlin.math.roundToInt - -private val TAG = ScanSurfaceView::class.simpleName - -private const val TIME_POST_PICTURE = 1500L -private const val DEFAULT_TIME_POST_PICTURE = 1500L -private const val IMAGE_ANALYSIS_SCALE_WIDTH = 400 - -internal class ScanSurfaceView : FrameLayout { - - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) - - private val binding: ScanSurfaceViewBinding - - lateinit var lifecycleOwner: LifecycleOwner - lateinit var listener: ScanSurfaceListener - lateinit var originalImageFile: File - - private val nativeClass = OpenCvNativeBridge() - private var autoCaptureTimer: CountDownTimer? = null - private var millisLeft = 0L - private var isAutoCaptureScheduled = false - private var isCapturing = false - - private var imageAnalysis: ImageAnalysis? = null - private var camera: Camera? = null - private var imageCapture: ImageCapture? = null - private var preview: Preview? = null - private var cameraProvider: ProcessCameraProvider? = null - private lateinit var previewSize: android.util.Size - - var isAutoCaptureOn: Boolean = true - private var isFlashEnabled: Boolean = false - private var flashMode: Int = ImageCapture.FLASH_MODE_OFF - - init { - binding = ScanSurfaceViewBinding.inflate(LayoutInflater.from(context), this, true) - } - - fun start() = with(binding) { - viewFinder.post { - viewFinder.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) - previewSize = android.util.Size(viewFinder.width, viewFinder.height) - openCamera() - } - } - - private fun clearAndInvalidateCanvas() { - binding.scanCanvasView.clearShape() - } - - private fun openCamera() { - val cameraProviderFuture = ProcessCameraProvider.getInstance(context) - - cameraProviderFuture.addListener(Runnable { - cameraProvider = cameraProviderFuture.get() - - try { - bindCamera() - checkIfFlashIsPresent() - } catch (exc: Exception) { - Log.e(TAG, ErrorMessage.CAMERA_USE_CASE_BINDING_FAILED.error, exc) - listener.onError(DocumentScannerErrorModel(ErrorMessage.CAMERA_USE_CASE_BINDING_FAILED, exc)) - } - }, ContextCompat.getMainExecutor(context)) - } - - private fun bindCamera() { - cameraProvider?.unbindAll() - camera = null - setUseCases() - } - - private fun setImageCapture() { - if(imageCapture != null && cameraProvider?.isBound(imageCapture!!) == true) { - cameraProvider?.unbind(imageCapture) - } - - imageCapture = null - imageCapture = ImageCapture.Builder() - .setTargetRotation(Surface.ROTATION_0) - .setFlashMode(flashMode) - .build() - } - - fun unbindCamera() { - cameraProvider?.unbind(imageAnalysis) - } - - private fun setUseCases() { - preview = Preview.Builder() - .setTargetResolution(previewSize) - .setTargetRotation(Surface.ROTATION_0) - .build() - .also { - it.setSurfaceProvider(binding.viewFinder.surfaceProvider) - } - - setImageCapture() - - val aspectRatio: Float = previewSize.width / previewSize.height.toFloat() - val width = IMAGE_ANALYSIS_SCALE_WIDTH - val height = (width / aspectRatio).roundToInt() - - imageAnalysis = ImageAnalysis.Builder() - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .setTargetResolution(android.util.Size(width, height)) - .setTargetRotation(Surface.ROTATION_0) - .build() - - imageAnalysis?.setAnalyzer(ContextCompat.getMainExecutor(context), { image -> - if (isAutoCaptureOn) { - try { - val mat = image.yuvToRgba() - val originalPreviewSize = mat.size() - val largestQuad = nativeClass.detectLargestQuadrilateral(mat) - mat.release() - if (null != largestQuad) { - drawLargestRect(largestQuad.contour, largestQuad.points, originalPreviewSize) - } else { - clearAndInvalidateCanvas() - } - } catch (e: Exception) { - Log.e(TAG, ErrorMessage.DETECT_LARGEST_QUADRILATERAL_FAILED.error, e) - listener.onError(DocumentScannerErrorModel(ErrorMessage.DETECT_LARGEST_QUADRILATERAL_FAILED, e)) - clearAndInvalidateCanvas() - } - } else { - clearAndInvalidateCanvas() - } - image.close() - }) - - camera = cameraProvider!!.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageAnalysis, imageCapture) - } - - private fun drawLargestRect(approx: MatOfPoint2f, points: Array, stdSize: Size) { - // Attention: axis are swapped - val previewWidth = stdSize.height.toFloat() - val previewHeight = stdSize.width.toFloat() - - val resultWidth = max(previewWidth - points[0].y.toFloat(), previewWidth - points[1].y.toFloat()) - - min(previewWidth - points[2].y.toFloat(), previewWidth - points[3].y.toFloat()) - - val resultHeight = max(points[1].x.toFloat(), points[2].x.toFloat()) - min(points[0].x.toFloat(), points[3].x.toFloat()) - - val imgDetectionPropsObj = ImageDetectionProperties(previewWidth.toDouble(), previewHeight.toDouble(), - points[0], points[1], points[2], points[3], resultWidth.toInt(), resultHeight.toInt()) - if (imgDetectionPropsObj.isNotValidImage(approx)) { - binding.scanCanvasView.clearShape() - cancelAutoCapture() - } else { - if (!isAutoCaptureScheduled) { - scheduleAutoCapture() - } - binding.scanCanvasView.showShape(previewWidth, previewHeight, points) - } - } - - private fun scheduleAutoCapture() { - isAutoCaptureScheduled = true - millisLeft = 0L - autoCaptureTimer = object : CountDownTimer(DEFAULT_TIME_POST_PICTURE, 100) { - override fun onTick(millisUntilFinished: Long) { - if (millisUntilFinished != millisLeft) { - millisLeft = millisUntilFinished - } - } - - override fun onFinish() { - isAutoCaptureScheduled = false - autoCapture() - } - } - autoCaptureTimer?.start() - } - - private fun autoCapture() { - if (isCapturing) - return - cancelAutoCapture() - takePicture() - } - - fun takePicture() { - Log.d(TAG, "ZDCtakePicture Starts ${System.currentTimeMillis()}") - listener.scanSurfaceShowProgress() - isCapturing = true - - val imageCapture = imageCapture ?: return - val outputOptions = ImageCapture.OutputFileOptions.Builder(originalImageFile).build() - - imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(context), - object : ImageCapture.OnImageSavedCallback { - override fun onError(exc: ImageCaptureException) { - listener.scanSurfaceHideProgress() - Log.e(TAG, "${ErrorMessage.PHOTO_CAPTURE_FAILED.error}: ${exc.message}", exc) - listener.onError(DocumentScannerErrorModel(ErrorMessage.PHOTO_CAPTURE_FAILED, exc)) - } - - override fun onImageSaved(output: ImageCapture.OutputFileResults) { - listener.scanSurfaceHideProgress() - - unbindCamera() - - clearAndInvalidateCanvas() - listener.scanSurfacePictureTaken() - postDelayed({ isCapturing = false }, TIME_POST_PICTURE) - Log.d(TAG, "ZDCtakePicture ends ${System.currentTimeMillis()}") - } - }) - } - - private fun checkIfFlashIsPresent() { - if (camera?.cameraInfo?.hasFlashUnit() == true) { - listener.showFlash() - } else { - listener.hideFlash() - } - } - - private fun cancelAutoCapture() { - if (isAutoCaptureScheduled) { - isAutoCaptureScheduled = false - autoCaptureTimer?.cancel() - } - } - - fun switchFlashState() { - isFlashEnabled = !isFlashEnabled - flashMode = if (isFlashEnabled) { - listener.showFlashModeOn() - ImageCapture.FLASH_MODE_ON - } else { - listener.showFlashModeOff() - ImageCapture.FLASH_MODE_OFF - } - setImageCapture() - camera = cameraProvider!!.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, imageCapture) - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imagecrop/ImageCropFragment.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imagecrop/ImageCropFragment.kt deleted file mode 100644 index 8ab1ef6633..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imagecrop/ImageCropFragment.kt +++ /dev/null @@ -1,159 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.imagecrop - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.PointF -import android.graphics.drawable.BitmapDrawable -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.util.Log -import android.view.Gravity -import android.view.View -import android.widget.FrameLayout -import com.zynksoftware.documentscanner.R -import com.zynksoftware.documentscanner.ScanActivity -import com.zynksoftware.documentscanner.common.extensions.scaledBitmap -import com.zynksoftware.documentscanner.common.utils.OpenCvNativeBridge -import com.zynksoftware.documentscanner.databinding.FragmentImageCropBinding -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel -import com.zynksoftware.documentscanner.ui.base.BaseFragment -import com.zynksoftware.documentscanner.ui.scan.InternalScanActivity -import id.zelory.compressor.determineImageRotation - -internal class ImageCropFragment : BaseFragment(FragmentImageCropBinding::inflate) { - - private val nativeClass = OpenCvNativeBridge() - - private var selectedImage: Bitmap? = null - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val sourceBitmap = BitmapFactory.decodeFile(getScanActivity().originalImageFile.absolutePath) - if (sourceBitmap != null) { - selectedImage = determineImageRotation(getScanActivity().originalImageFile, sourceBitmap) - } else { - Log.e(TAG, DocumentScannerErrorModel.ErrorMessage.INVALID_IMAGE.error) - onError(DocumentScannerErrorModel(DocumentScannerErrorModel.ErrorMessage.INVALID_IMAGE)) - Handler(Looper.getMainLooper()).post{ - closeFragment() - } - } - binding.holderImageView.post { - if (this.view != null) initializeCropping() - } - - initListeners() - } - - private fun initListeners() = with(binding) { - closeButton.setOnClickListener { - closeFragment() - } - confirmButton.setOnClickListener { - onConfirmButtonClicked() - } - } - - private fun getScanActivity(): InternalScanActivity { - return (requireActivity() as InternalScanActivity) - } - - private fun initializeCropping() = with(binding) { - if(selectedImage != null && selectedImage!!.width > 0 && selectedImage!!.height > 0) { - val scaledBitmap: Bitmap = selectedImage!!.scaledBitmap(holderImageCrop.width, holderImageCrop.height) - imagePreview.setImageBitmap(scaledBitmap) - val tempBitmap = (imagePreview.drawable as BitmapDrawable).bitmap - val pointFs = getEdgePoints(tempBitmap) - Log.d(TAG, "ZDCgetEdgePoints ends ${System.currentTimeMillis()}") - polygonView.setPoints(pointFs) - polygonView.visibility = View.VISIBLE - val padding = resources.getDimension(R.dimen.zdc_polygon_dimens).toInt() - val layoutParams = FrameLayout.LayoutParams(tempBitmap.width + padding, tempBitmap.height + padding) - layoutParams.gravity = Gravity.CENTER - polygonView.layoutParams = layoutParams - } - } - - private fun onError(error: DocumentScannerErrorModel) { - if (isAdded) { - getScanActivity().onError(error) - } - } - - private fun onConfirmButtonClicked() { - getCroppedImage() - (activity as ScanActivity).finalScannerResult() - } - - private fun getEdgePoints(tempBitmap: Bitmap): Map { - Log.d(TAG, "ZDCgetEdgePoints Starts ${System.currentTimeMillis()}") - val pointFs: List = nativeClass.getContourEdgePoints(tempBitmap) - return binding.polygonView.getOrderedValidEdgePoints(tempBitmap, pointFs) - } - - private fun getCroppedImage() { - if(selectedImage != null) { - try { - Log.d(TAG, "ZDCgetCroppedImage starts ${System.currentTimeMillis()}") - val points: Map = binding.polygonView.getPoints() - val xRatio: Float = selectedImage!!.width.toFloat() / binding.imagePreview.width - val yRatio: Float = selectedImage!!.height.toFloat() / binding.imagePreview.height - val pointPadding = requireContext().resources.getDimension(R.dimen.zdc_point_padding).toInt() - val x1: Float = (points.getValue(0).x + pointPadding) * xRatio - val x2: Float = (points.getValue(1).x + pointPadding) * xRatio - val x3: Float = (points.getValue(2).x + pointPadding) * xRatio - val x4: Float = (points.getValue(3).x + pointPadding) * xRatio - val y1: Float = (points.getValue(0).y + pointPadding) * yRatio - val y2: Float = (points.getValue(1).y + pointPadding) * yRatio - val y3: Float = (points.getValue(2).y + pointPadding) * yRatio - val y4: Float = (points.getValue(3).y + pointPadding) * yRatio - getScanActivity().croppedImage = nativeClass.getScannedBitmap(selectedImage!!, x1, y1, x2, y2, x3, y3, x4, y4) - Log.d(TAG, "ZDCgetCroppedImage ends ${System.currentTimeMillis()}") - } catch (e: java.lang.Exception) { - Log.e(TAG, DocumentScannerErrorModel.ErrorMessage.CROPPING_FAILED.error, e) - onError(DocumentScannerErrorModel(DocumentScannerErrorModel.ErrorMessage.CROPPING_FAILED, e)) - } - } else { - Log.e(TAG, DocumentScannerErrorModel.ErrorMessage.INVALID_IMAGE.error) - onError(DocumentScannerErrorModel(DocumentScannerErrorModel.ErrorMessage.INVALID_IMAGE)) - } - } - - private fun startImageProcessingFragment() { - getScanActivity().showImageProcessingFragment() - } - - private fun closeFragment() { - getScanActivity().closeCurrentFragment() - } - - companion object { - private val TAG = ImageCropFragment::class.simpleName - - fun newInstance(): ImageCropFragment { - return ImageCropFragment() - } - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imageprocessing/ImageProcessingFragment.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imageprocessing/ImageProcessingFragment.kt deleted file mode 100644 index 735a8fb1d6..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imageprocessing/ImageProcessingFragment.kt +++ /dev/null @@ -1,136 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.imageprocessing - -import android.graphics.* -import android.os.Bundle -import android.util.Log -import android.view.View -import com.zynksoftware.documentscanner.common.extensions.rotateBitmap -import com.zynksoftware.documentscanner.databinding.FragmentImageProcessingBinding -import com.zynksoftware.documentscanner.ui.base.BaseFragment -import com.zynksoftware.documentscanner.ui.scan.InternalScanActivity -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch - -internal class ImageProcessingFragment : BaseFragment(FragmentImageProcessingBinding::inflate) { - - private var isInverted = false - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.imagePreview.setImageBitmap(getScanActivity().croppedImage) - - initListeners() - } - - private fun initListeners() = with(binding) { - closeButton.setOnClickListener { - closeFragment() - } - confirmButton.setOnClickListener { - selectFinalScannerResults() - } - magicButton.setOnClickListener { - applyGrayScaleFilter() - } - rotateButton.setOnClickListener { - rotateImage() - } - } - - private fun getScanActivity(): InternalScanActivity { - return (requireActivity() as InternalScanActivity) - } - - private fun rotateImage() { - Log.d(TAG, "ZDCrotate starts ${System.currentTimeMillis()}") - showProgressBar() - GlobalScope.launch(Dispatchers.IO) { - if(isAdded) { - getScanActivity().transformedImage = getScanActivity().transformedImage?.rotateBitmap(ANGLE_OF_ROTATION) - getScanActivity().croppedImage = getScanActivity().croppedImage?.rotateBitmap(ANGLE_OF_ROTATION) - } - - if(isAdded) { - getScanActivity().runOnUiThread { - hideProgressBar() - if (isInverted) { - binding.imagePreview?.setImageBitmap(getScanActivity().transformedImage) - } else { - binding.imagePreview?.setImageBitmap(getScanActivity().croppedImage) - } - } - } - Log.d(TAG, "ZDCrotate ends ${System.currentTimeMillis()}") - } - } - - private fun closeFragment() { - getScanActivity().closeCurrentFragment() - } - - private fun applyGrayScaleFilter() { - Log.d(TAG, "ZDCgrayscale starts ${System.currentTimeMillis()}") - showProgressBar() - GlobalScope.launch(Dispatchers.IO) { - if(isAdded) { - if (!isInverted) { - val bmpMonochrome = Bitmap.createBitmap(getScanActivity().croppedImage!!.width, getScanActivity().croppedImage!!.height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bmpMonochrome) - val ma = ColorMatrix() - ma.setSaturation(0f) - val paint = Paint() - paint.colorFilter = ColorMatrixColorFilter(ma) - getScanActivity().croppedImage?.let { canvas.drawBitmap(it, 0f, 0f, paint) } - getScanActivity().transformedImage = - bmpMonochrome.config?.let { bmpMonochrome.copy(it, true) } - getScanActivity().runOnUiThread { - hideProgressBar() - binding.imagePreview.setImageBitmap(getScanActivity().transformedImage) - } - } else { - getScanActivity().runOnUiThread { - hideProgressBar() - binding.imagePreview.setImageBitmap(getScanActivity().croppedImage) - } - } - isInverted = !isInverted - Log.d(TAG, "ZDCgrayscale ends ${System.currentTimeMillis()}") - } - } - } - - private fun selectFinalScannerResults() { - getScanActivity().finalScannerResult() - } - - companion object { - private val TAG = ImageProcessingFragment::class.simpleName - private const val ANGLE_OF_ROTATION = 90 - - fun newInstance(): ImageProcessingFragment { - return ImageProcessingFragment() - } - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/scan/InternalScanActivity.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/scan/InternalScanActivity.kt deleted file mode 100644 index 6ae5012f43..0000000000 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/scan/InternalScanActivity.kt +++ /dev/null @@ -1,196 +0,0 @@ -/** -Copyright 2020 ZynkSoftware SRL - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -package com.zynksoftware.documentscanner.ui.scan - -import android.graphics.Bitmap -import android.os.Bundle -import android.util.Log -import android.widget.FrameLayout -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentTransaction -import com.zynksoftware.documentscanner.R -import com.zynksoftware.documentscanner.common.extensions.hide -import com.zynksoftware.documentscanner.common.extensions.show -import com.zynksoftware.documentscanner.manager.SessionManager -import com.zynksoftware.documentscanner.model.DocumentScannerErrorModel -import com.zynksoftware.documentscanner.model.ScannerResults -import com.zynksoftware.documentscanner.ui.camerascreen.CameraScreenFragment -import com.zynksoftware.documentscanner.ui.components.ProgressView -import com.zynksoftware.documentscanner.ui.imagecrop.ImageCropFragment -import com.zynksoftware.documentscanner.ui.imageprocessing.ImageProcessingFragment -import id.zelory.compressor.Compressor -import id.zelory.compressor.constraint.format -import id.zelory.compressor.constraint.quality -import id.zelory.compressor.constraint.size -import id.zelory.compressor.extension -import id.zelory.compressor.saveBitmap -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import java.io.File - -abstract class InternalScanActivity : AppCompatActivity() { - - abstract fun onError(error: DocumentScannerErrorModel) - abstract fun onSuccess(scannerResults: ScannerResults) - abstract fun onClose() - - companion object { - private val TAG = InternalScanActivity::class.simpleName - internal const val CAMERA_SCREEN_FRAGMENT_TAG = "CameraScreenFragmentTag" - internal const val IMAGE_CROP_FRAGMENT_TAG = "ImageCropFragmentTag" - internal const val IMAGE_PROCESSING_FRAGMENT_TAG = "ImageProcessingFragmentTag" - internal const val ORIGINAL_IMAGE_NAME = "original" - internal const val CROPPED_IMAGE_NAME = "cropped" - internal const val TRANSFORMED_IMAGE_NAME = "transformed" - internal const val NOT_INITIALIZED = -1L - } - - internal lateinit var originalImageFile: File - internal var croppedImage: Bitmap? = null - internal var transformedImage: Bitmap? = null - private var imageQuality: Int = 100 - private var imageSize: Long = NOT_INITIALIZED - private lateinit var imageType: Bitmap.CompressFormat - internal var shouldCallOnClose = true - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val sessionManager = SessionManager(this) - imageType = sessionManager.getImageType() - imageSize = sessionManager.getImageSize() - imageQuality = sessionManager.getImageQuality() - reInitOriginalImageFile() - } - - internal fun reInitOriginalImageFile() { - originalImageFile = File(filesDir, "${ORIGINAL_IMAGE_NAME}_${System.currentTimeMillis()}.${imageType.extension()}") - } - - private fun showCameraScreen() { - val cameraScreenFragment = CameraScreenFragment.newInstance() - addFragmentToBackStack(cameraScreenFragment, CAMERA_SCREEN_FRAGMENT_TAG) - } - - internal fun showImageCropFragment() { - val imageCropFragment = ImageCropFragment.newInstance() - addFragmentToBackStack(imageCropFragment, IMAGE_CROP_FRAGMENT_TAG) - } - - internal fun showImageProcessingFragment() { - val imageProcessingFragment = ImageProcessingFragment.newInstance() - addFragmentToBackStack(imageProcessingFragment, IMAGE_PROCESSING_FRAGMENT_TAG) - } - - internal fun closeCurrentFragment() { - supportFragmentManager.popBackStackImmediate() - } - - private fun addFragmentToBackStack(fragment: Fragment, fragmentTag: String) { - val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction() - fragmentTransaction.replace(R.id.zdcContent, fragment, fragmentTag) - if (supportFragmentManager.findFragmentByTag(fragmentTag) == null) { - fragmentTransaction.addToBackStack(fragmentTag) - } - fragmentTransaction.commit() - } - - internal fun finalScannerResult() { - findViewById(R.id.zdcContent).hide() - compressFiles() - } - - private fun compressFiles() { - Log.d(TAG, "ZDCcompress starts ${System.currentTimeMillis()}") - findViewById(R.id.zdcProgressView).show() - GlobalScope.launch(Dispatchers.IO) { - var croppedImageFile: File? = null - croppedImage?.let { - croppedImageFile = File(filesDir, "${CROPPED_IMAGE_NAME}_${System.currentTimeMillis()}.${imageType.extension()}") - saveBitmap(it, croppedImageFile!!, imageType, imageQuality) - } - - var transformedImageFile: File? = null - transformedImage?.let { - transformedImageFile = File(filesDir, "${TRANSFORMED_IMAGE_NAME}_${System.currentTimeMillis()}.${imageType.extension()}") - saveBitmap(it, transformedImageFile!!, imageType, imageQuality) - } - - originalImageFile = Compressor.compress(this@InternalScanActivity, originalImageFile) { - quality(imageQuality) - if (imageSize != NOT_INITIALIZED) size(imageSize) - format(imageType) - } - - croppedImageFile = croppedImageFile?.let { - Compressor.compress(this@InternalScanActivity, it) { - quality(imageQuality) - if (imageSize != NOT_INITIALIZED) size(imageSize) - format(imageType) - } - } - - transformedImageFile = transformedImageFile?.let { - Compressor.compress(this@InternalScanActivity, it) { - quality(imageQuality) - if (imageSize != NOT_INITIALIZED) size(imageSize) - format(imageType) - } - } - - val scannerResults = ScannerResults(originalImageFile, croppedImageFile, transformedImageFile) - runOnUiThread { - findViewById(R.id.zdcProgressView).hide() - shouldCallOnClose = false - supportFragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - shouldCallOnClose = true - onSuccess(scannerResults) - Log.d(TAG, "ZDCcompress ends ${System.currentTimeMillis()}") - } - } - } - - internal fun addFragmentContentLayoutInternal() { - val frameLayout = FrameLayout(this) - frameLayout.id = R.id.zdcContent - addContentView( - frameLayout, FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT - ) - ) - - val progressView = ProgressView(this) - progressView.id = R.id.zdcProgressView - addContentView( - progressView, FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT - ) - ) - - progressView.hide() - - showCameraScreen() - } -} \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/drawable/camera_button_circle.xml b/libs/DocumentScanner/src/main/res/drawable/camera_button_circle.xml deleted file mode 100644 index 5bf8734e68..0000000000 --- a/libs/DocumentScanner/src/main/res/drawable/camera_button_circle.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/drawable/crop_corner_circle.xml b/libs/DocumentScanner/src/main/res/drawable/crop_corner_circle.xml deleted file mode 100644 index c42fe5877a..0000000000 --- a/libs/DocumentScanner/src/main/res/drawable/crop_corner_circle.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/drawable/iconclose.png b/libs/DocumentScanner/src/main/res/drawable/iconclose.png deleted file mode 100644 index 0297199d79..0000000000 Binary files a/libs/DocumentScanner/src/main/res/drawable/iconclose.png and /dev/null differ diff --git a/libs/DocumentScanner/src/main/res/drawable/zdc_flash_off.png b/libs/DocumentScanner/src/main/res/drawable/zdc_flash_off.png deleted file mode 100644 index ee5fe4afec..0000000000 Binary files a/libs/DocumentScanner/src/main/res/drawable/zdc_flash_off.png and /dev/null differ diff --git a/libs/DocumentScanner/src/main/res/drawable/zdc_flash_on.png b/libs/DocumentScanner/src/main/res/drawable/zdc_flash_on.png deleted file mode 100644 index 7dd46e03b2..0000000000 Binary files a/libs/DocumentScanner/src/main/res/drawable/zdc_flash_on.png and /dev/null differ diff --git a/libs/DocumentScanner/src/main/res/drawable/zdc_gallery_icon.png b/libs/DocumentScanner/src/main/res/drawable/zdc_gallery_icon.png deleted file mode 100644 index 0ef7562371..0000000000 Binary files a/libs/DocumentScanner/src/main/res/drawable/zdc_gallery_icon.png and /dev/null differ diff --git a/libs/DocumentScanner/src/main/res/drawable/zdc_magic_wand_icon.png b/libs/DocumentScanner/src/main/res/drawable/zdc_magic_wand_icon.png deleted file mode 100644 index 68a9b36a04..0000000000 Binary files a/libs/DocumentScanner/src/main/res/drawable/zdc_magic_wand_icon.png and /dev/null differ diff --git a/libs/DocumentScanner/src/main/res/drawable/zdc_rotation_icon.png b/libs/DocumentScanner/src/main/res/drawable/zdc_rotation_icon.png deleted file mode 100644 index 2f8a18d82b..0000000000 Binary files a/libs/DocumentScanner/src/main/res/drawable/zdc_rotation_icon.png and /dev/null differ diff --git a/libs/DocumentScanner/src/main/res/drawable/zdc_tick_icon.png b/libs/DocumentScanner/src/main/res/drawable/zdc_tick_icon.png deleted file mode 100644 index 15376f01c0..0000000000 Binary files a/libs/DocumentScanner/src/main/res/drawable/zdc_tick_icon.png and /dev/null differ diff --git a/libs/DocumentScanner/src/main/res/layout/fragment_camera_screen.xml b/libs/DocumentScanner/src/main/res/layout/fragment_camera_screen.xml deleted file mode 100644 index 3ed89f9606..0000000000 --- a/libs/DocumentScanner/src/main/res/layout/fragment_camera_screen.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/layout/fragment_image_crop.xml b/libs/DocumentScanner/src/main/res/layout/fragment_image_crop.xml deleted file mode 100644 index 726f2648a0..0000000000 --- a/libs/DocumentScanner/src/main/res/layout/fragment_image_crop.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/layout/fragment_image_processing.xml b/libs/DocumentScanner/src/main/res/layout/fragment_image_processing.xml deleted file mode 100644 index 34901f292d..0000000000 --- a/libs/DocumentScanner/src/main/res/layout/fragment_image_processing.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/libs/DocumentScanner/src/main/res/layout/progress_layout.xml b/libs/DocumentScanner/src/main/res/layout/progress_layout.xml deleted file mode 100644 index 09ba456ec0..0000000000 --- a/libs/DocumentScanner/src/main/res/layout/progress_layout.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/layout/scan_surface_view.xml b/libs/DocumentScanner/src/main/res/layout/scan_surface_view.xml deleted file mode 100644 index f0274c73b4..0000000000 --- a/libs/DocumentScanner/src/main/res/layout/scan_surface_view.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/values-night/colors.xml b/libs/DocumentScanner/src/main/res/values-night/colors.xml deleted file mode 100644 index 9444cbd565..0000000000 --- a/libs/DocumentScanner/src/main/res/values-night/colors.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - #232323 - #FFFFFF - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/values/colors.xml b/libs/DocumentScanner/src/main/res/values/colors.xml deleted file mode 100644 index 1e7ce9d650..0000000000 --- a/libs/DocumentScanner/src/main/res/values/colors.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - #99000000 - #afadae - - #E9001C - - #77ffffff - - #FFFFFF - #000000 - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/values/dimens.xml b/libs/DocumentScanner/src/main/res/values/dimens.xml deleted file mode 100644 index 6bd0df5ac0..0000000000 --- a/libs/DocumentScanner/src/main/res/values/dimens.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - 2dp - 10dp - - 12sp - 14sp - 18sp - 50dp - - 80dp - 40dp - 20dp - 12dp - 10dp - 8dp - - 100dp - 0.5dp - 0dp - 15dp - 50dp - 40dp - 32dp - 1dp - 2dp - 4dp - 20dp - 50dp - 40dp - -20dp - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/values/ids.xml b/libs/DocumentScanner/src/main/res/values/ids.xml deleted file mode 100644 index 5f7ceae156..0000000000 --- a/libs/DocumentScanner/src/main/res/values/ids.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/values/strings.xml b/libs/DocumentScanner/src/main/res/values/strings.xml deleted file mode 100644 index 1a6465b0bd..0000000000 --- a/libs/DocumentScanner/src/main/res/values/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - Cancel - Auto - Manual - \ No newline at end of file diff --git a/libs/DocumentScanner/src/main/res/values/styles.xml b/libs/DocumentScanner/src/main/res/values/styles.xml deleted file mode 100644 index 6070315a53..0000000000 --- a/libs/DocumentScanner/src/main/res/values/styles.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/libs/annotations/build.gradle b/libs/annotations/build.gradle index 072df18231..9fbeaa9100 100644 --- a/libs/annotations/build.gradle +++ b/libs/annotations/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.devtools.ksp' static String isTesting() { if ( System.getenv("IS_TESTING") == "true" ) { @@ -96,7 +96,7 @@ repositories { username pspdfMavenUser password pspdfMavenPass } - url 'https://customers.pspdfkit.com/maven/' + url 'https://my.nutrient.io/maven' } } @@ -105,7 +105,7 @@ dependencies { api project(path: ':pandautils') api project(path: ':pandares') - api Libs.PSPDFKIT + api Libs.NUTRIENT androidTestImplementation Libs.JUNIT testImplementation Libs.JUNIT diff --git a/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt b/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt index 99fabce95c..18774af417 100644 --- a/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt +++ b/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt @@ -115,7 +115,7 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation .setAnnotationInspectorEnabled(true) .layoutMode(PageLayoutMode.SINGLE) .textSelectionEnabled(false) - .disableCopyPaste() + .copyPastEnabled(false) .build() private val annotationCreationToolbar = AnnotationCreationToolbar(context) @@ -174,9 +174,8 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation protected fun unregisterPdfFragmentListeners() { pdfFragment?.removeOnAnnotationCreationModeChangeListener(this) pdfFragment?.removeOnAnnotationEditingModeChangeListener(this) - pdfFragment?.document?.annotationProvider?.removeOnAnnotationUpdatedListener(annotationUpdateListener) + pdfFragment?.removeOnAnnotationUpdatedListener(annotationUpdateListener) pdfFragment?.removeOnAnnotationSelectedListener(annotationSelectedListener) - pdfFragment?.removeOnAnnotationDeselectedListener(annotationDeselectedListener) } /** @@ -198,12 +197,12 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation if (toolbar is AnnotationCreationToolbar) { setUpGrabAnnotationTool(toolbar) + toolbar.setMenuItemGroupingRule(AnnotationCreationGroupingRule(context)) } } }) annotationCreationToolbar.closeButton.setGone() - annotationCreationToolbar.setMenuItemGroupingRule(AnnotationCreationGroupingRule(context)) annotationEditingToolbar.setMenuItemGroupingRule(object : MenuItemGroupingRule { override fun groupMenuItems(items: MutableList, i: Int) = configureEditMenuItemGrouping(items) @@ -415,9 +414,6 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation annotationsJob = tryWeave { // Snag them annotations with the session id val annotations = awaitApi { CanvaDocsManager.getAnnotations(apiValues.sessionId, apiValues.canvaDocsDomain, it) } - // We don't want to trigger the annotation events here, so unregister and re-register after - pdfFragment?.document?.annotationProvider?.removeOnAnnotationUpdatedListener(annotationUpdateListener) - // Grab all the annotations and sort them by type (descending). // This will result in all of the comments being iterated over first as the COMMENT_REPLY type is last in the AnnotationType enum. val sortedAnnotationList = annotations.data.sortedByDescending { it.annotationType } @@ -456,9 +452,8 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation } noteHinter?.notifyDrawablesChanged() - pdfFragment?.document?.annotationProvider?.addOnAnnotationUpdatedListener(annotationUpdateListener) + pdfFragment?.addOnAnnotationUpdatedListener(annotationUpdateListener) pdfFragment?.addOnAnnotationSelectedListener(annotationSelectedListener) - pdfFragment?.addOnAnnotationDeselectedListener(annotationDeselectedListener) } catch { // Show error toast(R.string.annotationErrorOccurred) @@ -618,10 +613,6 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation && commentRepliesHashMap[currentAnnotation.annotationId]?.isNotEmpty() == true } - private val annotationDeselectedListener = AnnotationManager.OnAnnotationDeselectedListener { _, _ -> - commentsButton.setGone() - } - //region Annotation Manipulation fun createNewAnnotation(annotation: Annotation) { if (annotation.type == AnnotationType.FREETEXT) { @@ -642,9 +633,7 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation // Edit the annotation with the appropriate id annotation.name = newAnnotation.annotationId - pdfFragment?.document?.annotationProvider?.removeOnAnnotationUpdatedListener(annotationUpdateListener) pdfFragment?.notifyAnnotationHasChanged(annotation) - pdfFragment?.document?.annotationProvider?.addOnAnnotationUpdatedListener(annotationUpdateListener) commentsButton.isEnabled = true if (annotation.type == AnnotationType.STAMP) { commentsButton.setVisible() @@ -1017,7 +1006,7 @@ abstract class PdfSubmissionView(context: Context, private val studentAnnotation // If the user has read/write/manage we want to let them delete (and only delete) non-authored annotations val annotation = pdfFragment?.selectedAnnotations?.get(0) - if (docSession.annotationMetadata?.canManage() == true && annotation?.flags?.contains(AnnotationFlags.LOCKED) == true) { + if (::docSession.isInitialized && docSession.annotationMetadata?.canManage() == true && annotation?.flags?.contains(AnnotationFlags.LOCKED) == true) { // We need to only return a list with the delete menu item delete = ContextualToolbarMenuItem.createSingleItem(context, View.generateViewId(), ContextCompat.getDrawable(context, R.drawable.ic_trash)!!, diff --git a/libs/canvas-api-2/build.gradle b/libs/canvas-api-2/build.gradle index 7f69f3ed91..a94afaacf2 100644 --- a/libs/canvas-api-2/build.gradle +++ b/libs/canvas-api-2/build.gradle @@ -19,7 +19,7 @@ apply plugin: 'com.android.library' apply plugin: 'com.apollographql.apollo' apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' -apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.devtools.ksp' apply plugin: 'dagger.hilt.android.plugin' def pineDebugBaseUrl = "https://pine-api-dev.domain-svcs.nonprod.inseng.io" @@ -202,10 +202,10 @@ dependencies { implementation Libs.FIREBASE_CONFIG implementation Libs.HILT - kapt Libs.HILT_COMPILER + ksp Libs.HILT_COMPILER implementation Libs.ROOM - kapt Libs.ROOM_COMPILER + ksp Libs.ROOM_COMPILER implementation Libs.PENDO diff --git a/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/DifferentiationTagsQuery.graphql b/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/DifferentiationTagsQuery.graphql new file mode 100644 index 0000000000..72410894a9 --- /dev/null +++ b/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/DifferentiationTagsQuery.graphql @@ -0,0 +1,36 @@ +# +# Copyright (C) 2025 - present Instructure, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +query DifferentiationTagsQuery($courseId: ID!) { + course(id: $courseId) { + groupSets(includeNonCollaborative: true) { + _id + name + nonCollaborative + groups { + _id + name + nonCollaborative + membersConnection { + nodes { + user { + _id + } + } + } + } + } + } +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/HorizonGetCoursesQuery.graphql b/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/HorizonGetCoursesQuery.graphql index c82370bede..c766b73c1a 100644 --- a/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/HorizonGetCoursesQuery.graphql +++ b/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/HorizonGetCoursesQuery.graphql @@ -4,6 +4,7 @@ query GetCoursesQuery($id: ID!) { enrollments(currentOnly: false, horizonCourses: true) { id: _id state + lastActivityAt course { id: _id name diff --git a/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/ModuleItemCheckpointsQuery.graphql b/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/ModuleItemCheckpointsQuery.graphql new file mode 100644 index 0000000000..c04675189c --- /dev/null +++ b/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/ModuleItemCheckpointsQuery.graphql @@ -0,0 +1,29 @@ +query ModuleItemCheckpointsQuery($courseId: ID!, $pageSize: Int!, $nextCursor: String) { + course(id: $courseId) { + modulesConnection(first: $pageSize, after: $nextCursor) { + pageInfo { + endCursor + startCursor + hasNextPage + hasPreviousPage + } + edges { + node { + moduleItems { + _id + content { + ... on Discussion { + id + checkpoints { + dueAt + tag + pointsPossible + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/schema.json b/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/schema.json index 0d6b41ac99..9beabebbb7 100644 --- a/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/schema.json +++ b/libs/canvas-api-2/src/main/graphql/com/instructure/canvasapi2/schema.json @@ -15143,6 +15143,26 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "checkpoints", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Checkpoint", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "childTopics", "description": null, diff --git a/libs/canvas-api-2/src/main/graphql/com/instructure/journey/CreateWidget.graphql b/libs/canvas-api-2/src/main/graphql/com/instructure/journey/CreateWidget.graphql new file mode 100644 index 0000000000..372f875f75 --- /dev/null +++ b/libs/canvas-api-2/src/main/graphql/com/instructure/journey/CreateWidget.graphql @@ -0,0 +1,13 @@ +mutation CreateWidget( + $input: CreateWidgetInput!, + $userAccountId: String! +) { + createWidget(input: $input, userAccountId: $userAccountId) { + id + name + type + position + createdAt + updatedAt + } +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/graphql/com/instructure/journey/GetSkills.graphql b/libs/canvas-api-2/src/main/graphql/com/instructure/journey/GetSkills.graphql new file mode 100644 index 0000000000..d06b41aa9d --- /dev/null +++ b/libs/canvas-api-2/src/main/graphql/com/instructure/journey/GetSkills.graphql @@ -0,0 +1,9 @@ +query GetSkills($completedOnly: Boolean) { + skills(completedOnly: $completedOnly) { + id + name + proficiencyLevel + createdAt + updatedAt + } +} diff --git a/libs/canvas-api-2/src/main/graphql/com/instructure/journey/GetWidgetData.graphql b/libs/canvas-api-2/src/main/graphql/com/instructure/journey/GetWidgetData.graphql new file mode 100644 index 0000000000..4b4f0c2818 --- /dev/null +++ b/libs/canvas-api-2/src/main/graphql/com/instructure/journey/GetWidgetData.graphql @@ -0,0 +1,18 @@ +query GetWidgetData( + $widgetType: String! + $dataScope: String! + $timeSpan: TimeSpanInput! + $canvasAccountId: String + $queryParams: WidgetDataFiltersInput +) { + widgetData( + widgetType: $widgetType + dataScope: $dataScope + timeSpan: $timeSpan + canvasAccountId: $canvasAccountId + queryParams: $queryParams + ) { + data + lastModifiedDate + } +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/graphql/com/instructure/journey/getWidgetByTypeAndUser.graphql b/libs/canvas-api-2/src/main/graphql/com/instructure/journey/getWidgetByTypeAndUser.graphql new file mode 100644 index 0000000000..f6472ef80e --- /dev/null +++ b/libs/canvas-api-2/src/main/graphql/com/instructure/journey/getWidgetByTypeAndUser.graphql @@ -0,0 +1,16 @@ +query GetWidgetByTypeAndUser( + $userAccountId: String! + $type: String! +) { + widgetByTypeAndUser( + userAccountId: $userAccountId + type: $type + ) { + id + name + type + position + createdAt + updatedAt + } +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/graphql/com/instructure/journey/schema.graphqls b/libs/canvas-api-2/src/main/graphql/com/instructure/journey/schema.graphqls index f10e0d0c7d..4860a4b570 100644 --- a/libs/canvas-api-2/src/main/graphql/com/instructure/journey/schema.graphqls +++ b/libs/canvas-api-2/src/main/graphql/com/instructure/journey/schema.graphqls @@ -2,1162 +2,1315 @@ directive @oneOf on INPUT_OBJECT type WidgetParamsGQL { - courseId: String - userId: String + courseIds: [Float!] + courseId: Float + userId: String + maxLateAssignments: Float + maxMissingAssignments: Float + minLowScorePercentage: Float + delayDaysAfterAction: Float + year: Float + month: Float } -type SkillTaxonomyAccountGQL { - # Unique identifier for the account - id: ID! - - # Root account UUID for the skill taxonomy - rootAccountUuid: String! - - # Creation date of the skill taxonomy account - createdAt: DateTime! +type ContentLibraryGQL { + id: ID! + accountId: Float! + courseId: Float! + externalLinksModuleId: Float + externalToolsModuleId: Float +} - # Groups associated with the skill taxonomy account - groups: [SkillTaxonomyGroupGQL!]! +type ContentVersionGQL { + id: ID! + assetId: Float! + assetType: AssetTypeEnum! + contentExportId: String! + title: String! + createdAt: DateTime! + courseId: Float +} - # Skills associated with the skill taxonomy account - taxonomySkills: [SkillTaxonomySkillGQL!]! +enum AssetTypeEnum { + assignment + discussion_topic + page + quiz + module + module_item + external_url + external_tool + file } # A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. scalar DateTime -type SkillTaxonomyGroupGQL { - # Unique identifier for the group - id: ID! - - # Name of the skill taxonomy group - name: String! +type ContentVersionPreviewGQL { + url: String +} - # Creation date of the group - createdAt: DateTime! +type AssetLinkGQL { + id: ID! + assetId: Float! + assetType: String! + courseId: Float! + contentLibraryCourseId: Float! + detached: Boolean! + createdAt: DateTime! +} - # Last updated date of the group - updatedAt: DateTime! +type CrmJwtGQL { + # JWT for CRM integration configuration + token: String! +} - # Account associated with the skill taxonomy group - account: SkillTaxonomyAccountGQL +type WidgetGroupGQL { + id: ID! + name: String + description: String + createdAt: DateTime! + widgets: [WidgetGQL!] +} - # Skills associated with the skill taxonomy group - taxonomySkills: [SkillTaxonomySkillGQL!]! +type WidgetGQL { + id: ID! + name: String! + type: String! + queryParams: WidgetParamsGQL! + position: Int + refreshIntervalMinutes: Int + createdAt: DateTime! + updatedAt: DateTime! + group: WidgetGroupGQL } -type SkillTaxonomySkillGQL { - # Unique identifier for the skill - id: ID! +type WidgetDataResultGQL { + # The most recent last modified timestamp among the underlying S3 objects used for this widget data + lastModifiedDate: DateTime - # Name of the skill - name: String! + # The widget data array (unchanged from the legacy getWidgetData) + data: [JSON!]! +} - # Description of the skill - description: String +# The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +scalar JSON - # Creation date of the skill - createdAt: DateTime! +type WidgetActionGQL { + id: ID! + action: String! + canvasUserUuid: String! + courseId: String! + actionReason: String! + createdAt: DateTime! + updatedAt: DateTime! +} - # Last updated date of the skill - updatedAt: DateTime! +type InsightActionModel { + id: ID! + type: String! + status: String! + errors: [String!] + issuedAt: DateTime! + input: InsightActionInputModel! +} - # Group to which the skill belongs - group: SkillTaxonomyGroupGQL +union InsightActionInputModel = + EncourageLearnersInput + | PraiseLearnersInput + | RemindLearnersInput - # Account associated with the skill - account: SkillTaxonomyAccountGQL - courses: [SkillTaxonomyCourseGQL!]! - programs: [ProgramGQL!]! - alignmentsGroupedByProficiencyScaleValue( - courseId: String! - ): [AlignmentsByProficiencyScaleValueGQL!]! - coursesGroupedByProficiencyScaleValue( - programId: String! - ): [CoursesByProficiencyScaleValueGQL!]! +type EncourageLearnersInput { + subject: String! + body: String! + learner_ids: [String!]! + sendIndividualMessages: Boolean! } -type PopulatedSimilarSkillGQL { - # Unique identifier for the skill - id: ID +type PraiseLearnersInput { + subject: String! + body: String! + learner_ids: [String!]! + sendIndividualMessages: Boolean! +} - # Name of the skill - name: String! +type RemindLearnersInput { + subject: String! + body: String! + course_id: String + learner_ids: [String!]! + sendIndividualMessages: Boolean! +} - # Description of the skill - description: String +type InsightMessageModel { + id: ID! + type: String! + status: String! - # Creation date of the skill - createdAt: DateTime! + # Time at which the message had new data + generatedAt: DateTime! - # Last updated date of the skill - updatedAt: DateTime! + # Time at which this message had its status last transitioned + updatedAt: DateTime! + date: DateTime! @deprecated(reason: "Use generatedAt instead") + rootAccountId: String! + accountId: String! + data: InsightMessageDataModel! +} - # Group to which the skill belongs - group: SkillTaxonomyGroupGQL +union InsightMessageDataModel = + LearnersDidCompleteCourse + | LearnersDidGainSkills + | LearnersDidNotStartCourseYet + | LearnersDidReturnAfterIdling + | LearnersInCourseAreIdling + | MostPopularSkill + | PlatformEngagementTrend - # Account associated with the skill - account: SkillTaxonomyAccountGQL - courses: [SkillTaxonomyCourseGQL!]! - programs: [ProgramGQL!]! +type LearnersDidCompleteCourse { + course_id: String! + learner_ids: [String!]! } -type SkillGQL { - id: String! - name: String! - createdAt: DateTime! - updatedAt: DateTime! - taxonomySkill: SkillTaxonomySkillGQL - experiences: [ExperienceGQL!]! - proficiencyLevel: String +type LearnersDidGainSkills { + skill_count: Float! + learner_ids: [String!]! } -type SimilarSkillGroupGQL { - # List of similar skills in the group - similarSkills: [PopulatedSimilarSkillGQL!]! +type LearnersDidNotStartCourseYet { + course_id: String! + learner_ids: [String!]! } -type ChangeTaxonomySkillsResultGQL { - # List of similar skill groups found in the taxonomy - similarSkillGroups: [SimilarSkillGroupGQL!]! +type LearnersDidReturnAfterIdling { + learner_ids: [String!]! +} + +type LearnersInCourseAreIdling { + course_id: String! + learner_ids: [String!]! +} - # List of skills that were successfully persisted to the taxonomy - persistedSkills: [SkillTaxonomySkillGQL!]! +type MostPopularSkill { + skill_name: String! +} - # List of skill IDs that were deleted from the taxonomy - deletedSkills: [ID!] +type PlatformEngagementTrend { + trend: Float! } -type SkillTaxonomyProficiencyScaleValueGQL { - # Unique identifier for the proficiency scale value - id: ID! +type UserMetadataGQL { + # Unique identifier for the user metadata + id: ID! - # Level of the proficiency scale value - level: Int! + # ID of the associated metadata user + userId: ID! - # String value of the proficiency scale value - value: String! -} + # Key for the metadata + key: String! -type SkillTaxonomyProficiencyScaleGQL { - # Unique identifier for the proficiency scale - id: ID! + # Value for the metadata + value: String! - # Name of the proficiency scale - name: String! + # Timestamp when the record was created + createdAt: DateTime! - # Root account UUID for the proficiency scale - rootAccountUuid: String + # Timestamp when the record was last updated + updatedAt: DateTime! - # Values associated with the proficiency scale - values: [SkillTaxonomyProficiencyScaleValueGQL!]! + # Associated metadata user + user: MetadataUserGQL } -type SkillTaxonomyCourseGQL { - # Unique identifier for the course - id: String! +type MetadataUserGQL { + # Unique identifier for the metadata user; internal to the metadata service + id: ID! - # Unique identifier for the course in Canvas - courseId: String! + # UUID of the user's Canvas root account + canvasRootAccountUuid: ID! - # Learning objects associated with the course - learningObjects: [SkillTaxonomyLearningObjectGQL!]! - uniqueSkills: Float! - skills(moduleId: String, moduleItemId: String): [SkillTaxonomySkillGQL!]! -} + # User's Canvas UUID + canvasUserUuid: ID! -type SkillTaxonomyLearningObjectGQL { - # Unique identifier for the learning object - id: String! + # List of user's leaders Canvas UUIDs + leaderCanvasUserUuids: [ID!] - # Content ID of the learning object - contentId: String! + # Timestamp when the record was created + createdAt: DateTime! - # Type of the learning object - type: String! + # Timestamp when the record was last updated + updatedAt: DateTime! - # Alignments of the learning object with skills - alignments: [SkillTaxonomySkillAlignmentGQL!]! + # List of this user's metadata + metadata: [UserMetadataGQL!] +} - # Course associated with the learning object - course: SkillTaxonomyCourseGQL +type MetadataUsersGql { + # List of users + users: [MetadataUserGQL!]! } -type SkillTaxonomySkillAlignmentGQL { - # Unique identifier for the skill alignment - id: ID! +type KeyValuePairGQL { + # Metadata key + key: String! - # Module identifier for the skill alignment - moduleId: String! + # Metadata value + value: String! +} - # Module item identifier for the skill alignment - moduleItemId: String! +type CanvasUserGQL { + # Canvas user ID + id: ID! - # Skill associated with the alignment - skill: SkillTaxonomySkillGQL + # User name from Canvas + name: String - # Proficiency scale values for the skill alignment - proficiencyScaleValues: [SkillTaxonomyProficiencyScaleValueGQL!]! + # User email from Canvas + email: String - # Learning object associated with the skill alignment - learningObject: SkillTaxonomyLearningObjectGQL -} + # User sortable name from Canvas + sortableName: String -type AssociateSkillsToLearningObjectsResultGQL { - # List of alignments that were successfully persisted - persistedAlignments: [SkillTaxonomySkillAlignmentGQL!]! + # User short name from Canvas + shortName: String - # List of alignment IDs that were deleted - deletedAlignments: [ID!]! -} + # User login ID from Canvas + loginId: String -type AlignmentsByProficiencyScaleValueGQL { - # Proficiency scale value for the skill alignment - proficiencyScaleValue: SkillTaxonomyProficiencyScaleValueGQL + # User avatar URL from Canvas + avatarUrl: String - # List of skill alignments for the proficiency scale value - alignments: [SkillTaxonomySkillAlignmentGQL!]! -} + # Canvas user UUID + uuid: String -type CoursesByProficiencyScaleValueGQL { - # Proficiency scale value specified in the alignment for the skill - proficiencyScaleValue: SkillTaxonomyProficiencyScaleValueGQL + # SIS user ID from Canvas + sisUserId: String - # List of courses for the proficiency scale value - courses: [SkillTaxonomyCourseGQL!]! + # Integration ID from Canvas + integrationId: String } -type ExperienceGQL { - id: String! - type: String! - status: String! - skill: SkillGQL - createdAt: DateTime! - updatedAt: DateTime! - fileName: String - alignment: SkillTaxonomySkillAlignmentGQL -} +type PersonGQL { + # Canvas user ID + id: ID! -type ExtractedSkillGQL { - name: String! -} + # User name from Canvas + name: String -type CourseRecommendationGQL { - courseId: Float! - skills: [ExtractedSkillGQL!]! -} + # User email from Canvas + email: String -type CourseRecommendationsGQL { - recommendedCourses: [CourseRecommendationGQL!]! -} + # User sortable name from Canvas + sortableName: String -type UserGQL { - id: String! - canvasId: String! - canvasBaseUrl: String! - createdAt: DateTime! - updatedAt: DateTime! - isOnboarded: Boolean! -} + # User short name from Canvas + shortName: String -type ProgramGQL { - id: String! + # User login ID from Canvas + loginId: String - # Internal name of the program - name: String! + # User avatar URL from Canvas + avatarUrl: String - # Public-facing name of the program - publicName: String + # Canvas user UUID + uuid: ID - # Custom ID associated by the creator - customerId: String - description: String - publishStatus: String! - createdAt: DateTime! - updatedAt: DateTime! - owner: String! - startDate: DateTime - endDate: DateTime + # SIS user ID from Canvas + sisUserId: String - # Program variant type (linear or non-linear) - variant: ProgramVariantType! + # Integration ID from Canvas + integrationId: String - # Number of courses required for completion in non-linear programs - courseCompletionCount: Int + # User metadata from Journey + metadata: [KeyValuePairGQL!] - # Course enrollment type for the program - courseEnrollment: CourseEnrollmentType! - requirements: [ProgramRequirementGQL!]! - enrollments: [ProgramEnrollmentGQL!]! - progresses: [ProgramProgressGQL!]! - uniqueSkills: Float! - skills: [SkillTaxonomySkillGQL!]! -} + # Leaders for this user from Canvas + leaders: [CanvasUserGQL!] -# Type of program variant (linear or non-linear) -enum ProgramVariantType { - LINEAR - NON_LINEAR -} + # Number of direct subordinates for this user from Canvas + subordinateCount: Float -# Type of course enrollment (self-enrollment or auto-enrollment) -enum CourseEnrollmentType { - AUTO - SELF -} + # User timezone from Canvas + timeZone: String -type ContentLibraryGQL { - id: ID! - accountId: Float! - courseId: Float! - externalLinksModuleId: Float - externalToolsModuleId: Float -} + # Last login timestamp from Canvas + lastLogin: String -type ContentVersionGQL { - id: ID! - assetId: Float! - assetType: AssetTypeEnum! - contentExportId: String! - title: String! - createdAt: DateTime! - courseId: Float + # Account UUID from Journey metadata + accountUuid: String! } -enum AssetTypeEnum { - assignment - discussion_topic - page - quiz - module - module_item - external_url - external_tool - file -} +type PaginationInfoGQL { + # Current page number (1-indexed) + currentPage: Int! -type ContentVersionPreviewGQL { - url: String + # Total number of pages available + totalPages: Int + + # Whether there is a next page + hasNextPage: Boolean! + + # Whether there is a previous page + hasPreviousPage: Boolean! } -type AssetLinkGQL { - id: ID! - assetId: Float! - assetType: String! - courseId: Float! - contentLibraryCourseId: Float! - detached: Boolean! - createdAt: DateTime! +type PeopleResponseGQL { + # List of people (Canvas users enriched with metadata) + people: [PersonGQL!]! + + # Pagination information for the query + pagination: PaginationInfoGQL! } type ProgramEnrollmentGQL { - id: String! - createdAt: DateTime! - updatedAt: DateTime! - enrollee: String! + id: String! + createdAt: DateTime! + updatedAt: DateTime! + enrollee: String! } type ProgramCourseGQL { - id: String! - createdAt: DateTime! - updatedAt: DateTime! - canvasCourseId: String! - canvasUrl: String! + id: String! + createdAt: DateTime! + updatedAt: DateTime! + canvasCourseId: String! + canvasUrl: String! } type ProgramRequirementGQL { - id: String! - createdAt: DateTime! - updatedAt: DateTime! - isCompletionRequired: Boolean! - isPlaceholder: Boolean! - description: String + id: String! + createdAt: DateTime! + updatedAt: DateTime! + isCompletionRequired: Boolean! + isPlaceholder: Boolean! + description: String - # Course enrollment type for this requirement - courseEnrollment: String! - dependency: ProgramCourseGQL - dependent: ProgramCourseGQL! + # Course enrollment type for this requirement + courseEnrollment: String! + dependency: ProgramCourseGQL + dependent: ProgramCourseGQL! } type ProgramProgressGQL { - id: String! - requirement: ProgramRequirementGQL! - enrollment: ProgramEnrollmentGQL! - completionPercentage: Float! - courseEnrollmentStatus: ProgramProgressCourseEnrollmentStatus! - createdAt: DateTime! - updatedAt: DateTime! + id: String! + requirement: ProgramRequirementGQL! + enrollment: ProgramEnrollmentGQL! + completionPercentage: Float! + courseEnrollmentStatus: ProgramProgressCourseEnrollmentStatus! + createdAt: DateTime! + updatedAt: DateTime! } enum ProgramProgressCourseEnrollmentStatus { - ENROLLED - NOT_ENROLLED - BLOCKED + ENROLLED + NOT_ENROLLED + BLOCKED } -type ForceProgressRecalcResultGQL { - updated: [String!]! - failed: [String!]! -} +type ProgramGQL { + id: String! -type CrmJwtGQL { - # JWT for CRM integration configuration - token: String! + # Internal name of the program + name: String! + + # Public-facing name of the program + publicName: String + + # Custom ID associated by the creator + customerId: String + description: String + publishStatus: String! + createdAt: DateTime! + updatedAt: DateTime! + owner: String! + startDate: DateTime + endDate: DateTime + + # Program variant type (linear or non-linear) + variant: ProgramVariantType! + + # Number of courses required for completion in non-linear programs + courseCompletionCount: Int + + # Course enrollment type for the program + courseEnrollment: CourseEnrollmentType! + requirements: [ProgramRequirementGQL!]! + enrollments: [ProgramEnrollmentGQL!]! + progresses: [ProgramProgressGQL!]! + uniqueSkills: Float! + skills: [SkillTaxonomySkillGQL!]! } -type WidgetGroupGQL { - id: ID! - name: String! - description: String! - position: Float! - widgets: [WidgetGQL!] +# Type of program variant (linear or non-linear) +enum ProgramVariantType { + LINEAR + NON_LINEAR } -type WidgetGQL { - id: ID! - name: String! - type: String! - queryParams: WidgetParamsGQL! - position: Int - refreshIntervalMinutes: Int - createdAt: DateTime! - updatedAt: DateTime! - group: WidgetGroupGQL +# Type of course enrollment (self-enrollment or auto-enrollment) +enum CourseEnrollmentType { + AUTO + SELF } -type UserMetadataGQL { - # Unique identifier for the user metadata - id: ID! +type SkillTaxonomyAccountGQL { + # Unique identifier for the account + id: ID! - # ID of the associated metadata user - userId: ID! + # Root account UUID for the skill taxonomy + rootAccountUuid: String! - # Key for the metadata - key: String! + # Creation date of the skill taxonomy account + createdAt: DateTime! - # Value for the metadata - value: String! + # Groups associated with the skill taxonomy account + groups: [SkillTaxonomyGroupGQL!]! - # Timestamp when the record was created - createdAt: DateTime! + # Skills associated with the skill taxonomy account + taxonomySkills: [SkillTaxonomySkillGQL!]! +} - # Timestamp when the record was last updated - updatedAt: DateTime! +type SkillTaxonomyGroupGQL { + # Unique identifier for the group + id: ID! - # Associated metadata user - user: MetadataUserGQL -} + # Name of the skill taxonomy group + name: String! -type MetadataUserGQL { - # Unique identifier for the metadata user; internal to the metadata service - id: ID! + # Creation date of the group + createdAt: DateTime! - # UUID of the user's Canvas root account - canvasRootAccountUuid: ID! + # Last updated date of the group + updatedAt: DateTime! - # User's Canvas UUID - canvasUserUuid: ID! + # Account associated with the skill taxonomy group + account: SkillTaxonomyAccountGQL - # List of user's leaders Canvas UUIDs - leaderCanvasUserUuids: [ID!] + # Skills associated with the skill taxonomy group + taxonomySkills: [SkillTaxonomySkillGQL!]! +} - # Timestamp when the record was created - createdAt: DateTime! +type SkillTaxonomySkillGQL { + # Unique identifier for the skill + id: ID! - # Timestamp when the record was last updated - updatedAt: DateTime! + # Name of the skill + name: String! - # List of this user's metadata - metadata: [UserMetadataGQL!] -} + # Description of the skill + description: String -type MetadataUsersGql { - # List of users - users: [MetadataUserGQL!]! -} + # Creation date of the skill + createdAt: DateTime! -type KeyValuePairGQL { - # Metadata key - key: String! + # Last updated date of the skill + updatedAt: DateTime! - # Metadata value - value: String! + # Group to which the skill belongs + group: SkillTaxonomyGroupGQL + + # Account associated with the skill + account: SkillTaxonomyAccountGQL + courses: [SkillTaxonomyCourseGQL!]! + programs: [ProgramGQL!]! + alignmentsGroupedByProficiencyScaleValue( + courseId: String! + ): [AlignmentsByProficiencyScaleValueGQL!]! + coursesGroupedByProficiencyScaleValue( + programId: String! + ): [CoursesByProficiencyScaleValueGQL!]! } -type CanvasUserGQL { - # Canvas user ID - id: ID! +type PopulatedSimilarSkillGQL { + # Unique identifier for the skill + id: ID + + # Name of the skill + name: String! - # User name from Canvas - name: String + # Description of the skill + description: String - # User email from Canvas - email: String + # Creation date of the skill + createdAt: DateTime! - # User sortable name from Canvas - sortableName: String + # Last updated date of the skill + updatedAt: DateTime! - # User short name from Canvas - shortName: String + # Group to which the skill belongs + group: SkillTaxonomyGroupGQL - # User login ID from Canvas - loginId: String + # Account associated with the skill + account: SkillTaxonomyAccountGQL + courses: [SkillTaxonomyCourseGQL!]! + programs: [ProgramGQL!]! +} - # User avatar URL from Canvas - avatarUrl: String +type SimilarSkillGroupGQL { + # List of similar skills in the group + similarSkills: [PopulatedSimilarSkillGQL!]! +} - # Canvas user UUID - uuid: String +type ChangeTaxonomySkillsResultGQL { + # List of similar skill groups found in the taxonomy + similarSkillGroups: [SimilarSkillGroupGQL!]! - # SIS user ID from Canvas - sisUserId: String + # List of skills that were successfully persisted to the taxonomy + persistedSkills: [SkillTaxonomySkillGQL!]! - # Integration ID from Canvas - integrationId: String + # List of skill IDs that were deleted from the taxonomy + deletedSkills: [ID!] } -type PersonGQL { - # Canvas user ID - id: ID! +type SkillTaxonomyProficiencyScaleValueGQL { + # Unique identifier for the proficiency scale value + id: ID! - # User name from Canvas - name: String + # Level of the proficiency scale value + level: Int! - # User email from Canvas - email: String + # String value of the proficiency scale value + value: String! +} - # User sortable name from Canvas - sortableName: String +type SkillTaxonomyProficiencyScaleGQL { + # Unique identifier for the proficiency scale + id: ID! - # User short name from Canvas - shortName: String + # Name of the proficiency scale + name: String! - # User login ID from Canvas - loginId: String + # Root account UUID for the proficiency scale + rootAccountUuid: String - # User avatar URL from Canvas - avatarUrl: String + # Values associated with the proficiency scale + values: [SkillTaxonomyProficiencyScaleValueGQL!]! +} - # Canvas user UUID - uuid: ID +type SkillTaxonomyCourseGQL { + # Unique identifier for the course + id: String! - # SIS user ID from Canvas - sisUserId: String + # Unique identifier for the course in Canvas + courseId: String! - # Integration ID from Canvas - integrationId: String + # Learning objects associated with the course + learningObjects: [SkillTaxonomyLearningObjectGQL!]! + uniqueSkills: Float! + skills(moduleId: String, moduleItemId: String): [SkillTaxonomySkillGQL!]! +} - # User metadata from Journey - metadata: [KeyValuePairGQL!] +type SkillTaxonomyLearningObjectGQL { + # Unique identifier for the learning object + id: String! - # Leaders for this user from Canvas - leaders: [CanvasUserGQL!] + # Content ID of the learning object + contentId: String! - # User timezone from Canvas - timeZone: String + # Type of the learning object + type: String! - # Last login timestamp from Canvas - lastLogin: String + # Alignments of the learning object with skills + alignments: [SkillTaxonomySkillAlignmentGQL!]! - # Account UUID from Journey metadata - accountUuid: String! + # Course associated with the learning object + course: SkillTaxonomyCourseGQL } -type PaginationInfoGQL { - # Current page number (1-indexed) - currentPage: Int! +type SkillTaxonomySkillAlignmentGQL { + # Unique identifier for the skill alignment + id: ID! - # Total number of pages available - totalPages: Int + # Module identifier for the skill alignment + moduleId: String! - # Whether there is a next page - hasNextPage: Boolean! + # Module item identifier for the skill alignment + moduleItemId: String! - # Whether there is a previous page - hasPreviousPage: Boolean! -} + # Skill associated with the alignment + skill: SkillTaxonomySkillGQL -type PeopleResponseGQL { - # List of people (Canvas users enriched with metadata) - people: [PersonGQL!]! + # Proficiency scale values for the skill alignment + proficiencyScaleValues: [SkillTaxonomyProficiencyScaleValueGQL!]! - # Pagination information for the query - pagination: PaginationInfoGQL! + # Learning object associated with the skill alignment + learningObject: SkillTaxonomyLearningObjectGQL } -type InsightMessageModel { - id: ID! - type: String! - status: String! +type AssociateSkillsToLearningObjectsResultGQL { + # List of alignments that were successfully persisted + persistedAlignments: [SkillTaxonomySkillAlignmentGQL!]! - # Time at which the message had new data - generatedAt: DateTime! + # List of alignment IDs that were deleted + deletedAlignments: [ID!]! +} - # Time at which this message had its status last transitioned - updatedAt: DateTime! - date: DateTime! @deprecated(reason: "Use generatedAt instead") - rootAccountId: String! - accountId: String! - data: InsightMessageDataModel! +type AlignmentsByProficiencyScaleValueGQL { + # Proficiency scale value for the skill alignment + proficiencyScaleValue: SkillTaxonomyProficiencyScaleValueGQL + + # List of skill alignments for the proficiency scale value + alignments: [SkillTaxonomySkillAlignmentGQL!]! } -union InsightMessageDataModel = - LearnersDidCompleteCourse - | LearnersDidGainSkills - | LearnersDidNotStartCourseYet - | LearnersDidReturnAfterIdling - | LearnersInCourseAreIdling - | MostPopularSkill - | PlatformEngagementTrend +type CoursesByProficiencyScaleValueGQL { + # Proficiency scale value specified in the alignment for the skill + proficiencyScaleValue: SkillTaxonomyProficiencyScaleValueGQL -type LearnersDidCompleteCourse { - course_id: String! - learner_ids: [String!]! + # List of courses for the proficiency scale value + courses: [SkillTaxonomyCourseGQL!]! } -type LearnersDidGainSkills { - skill_count: Float! - learner_ids: [String!]! +type ForceProgressRecalcResultGQL { + updated: [String!]! + failed: [String!]! } -type LearnersDidNotStartCourseYet { - course_id: String! - learner_ids: [String!]! +type ExtractedSkillGQL { + name: String! } -type LearnersDidReturnAfterIdling { - learner_ids: [String!]! +type SkillGQL { + id: String! + name: String! + createdAt: DateTime! + updatedAt: DateTime! + taxonomySkill: SkillTaxonomySkillGQL + experiences: [ExperienceGQL!]! + proficiencyLevel: String } -type LearnersInCourseAreIdling { - course_id: String! - learner_ids: [String!]! +type UserGQL { + id: String! + canvasId: String! + canvasBaseUrl: String! + createdAt: DateTime! + updatedAt: DateTime! + isOnboarded: Boolean! } -type MostPopularSkill { - skill_name: String! +type CourseRecommendationGQL { + courseId: Float! + skills: [ExtractedSkillGQL!]! } -type PlatformEngagementTrend { - trend: Float! +type CourseRecommendationsGQL { + recommendedCourses: [CourseRecommendationGQL!]! +} + +type ExperienceGQL { + id: String! + type: String! + status: String! + skill: SkillGQL + createdAt: DateTime! + updatedAt: DateTime! + fileName: String + alignment: SkillTaxonomySkillAlignmentGQL } type Query { - skill(id: String!): SkillGQL - skills: [SkillGQL!]! - courseRecommendations( - proficiencyLevel: String - skillIds: [String!]! = [] - ): CourseRecommendationsGQL! - adminUsers(canvasBaseUrl: String!): [UserGQL!]! - currentUser: UserGQL! - - # Fetch all skills within the taxonomy for the given account - skillTaxonomySkills(accountId: String!): [SkillTaxonomySkillGQL!]! - - # Fetch all available proficiency scales - availableProficiencyScales( - accountId: String! - ): [SkillTaxonomyProficiencyScaleGQL!]! - - # Fetch the currently used proficiency scale for the account - currentlyUsedProficiencyScale: SkillTaxonomyProficiencyScaleGQL - skillTaxonomyGroups(accountId: String!): [SkillTaxonomyGroupGQL!]! - skillTaxonomyCourses(courseIds: [String!]!): [SkillTaxonomyCourseGQL!]! - skillTaxonomyCourse(courseId: String!): SkillTaxonomyCourseGQL! - - # Check if skill extraction has run for course/module/module item - hasSkillExtractionRun(input: SkillExtractionStatusInputGQL!): Boolean! - contentLibraries(accountId: Float!): [ContentLibraryGQL!]! - contentLibrary(courseId: Float!): ContentLibraryGQL! - contentVersions( - assetId: Float - assetType: AssetTypeEnum! - courseId: Float! - ): [ContentVersionGQL!]! - currentContentVersions( - assetType: AssetTypeEnum! - courseId: Float! - ): [ContentVersionGQL!]! - previewContentVersion(id: ID!): ContentVersionPreviewGQL! - assetLinks( - assetId: Float! - assetType: AssetTypeEnum! - contentLibraryCourseId: Float! - ): [AssetLinkGQL!]! - - # All programs available for the user to administer, optionally filtered by progress state - programs( - # Filter programs by progress state - filter: ProgramFilterInput - ): [ProgramGQL!]! - - # A program by id - program(programId: String!): ProgramGQL! - - # All user enrolled programs - enrolledPrograms: [ProgramGQL!]! - crmConfigJwt: CrmJwtGQL! - widgetFileUrl(key: String!): String! - widgetsByUser(userAccountId: String!): [WidgetGQL!]! - widgetData( - widgetId: String! - timeSpan: TimeSpanInput! - filters: WidgetDataFiltersInput - ): [JSON!]! - getAthenaQuery(queryExecutionId: String!): JSON! - testAthenaResult(key: String!): [JSON!]! - listS3Keys: [JSON!]! - widgetGroupsByUser(userAccountId: String!): [WidgetGroupGQL!]! - - # Get users (and metadata) matching the specified criteria - users(input: UsersQueryInputGQL!): MetadataUsersGql! - - # Get all distinct metadata key-value pairs for a given account - distinctMetadataValues(input: UsersQueryInputGQL!): [UserMetadataGQL!]! - - # Get users (and metadata) reporting to a specified user or the current user - userSubordinates(input: UserReportsQueryInputGQL!): MetadataUsersGql! - - # Get people (Canvas users enriched with metadata) from a specific account - getPeople(input: PeopleQueryInputGQL!): PeopleResponseGQL! - - # Retrieve all insight feed messages for the current user in a specific account. - insights(account: String!): [InsightMessageModel!]! + skill(id: String!): SkillGQL + skills(completedOnly: Boolean): [SkillGQL!]! + courseRecommendations( + proficiencyLevel: String + skillIds: [String!]! = [] + ): CourseRecommendationsGQL! + adminUsers(canvasBaseUrl: String!): [UserGQL!]! + currentUser: UserGQL! + + # Fetch all skills within the taxonomy for the given account + skillTaxonomySkills(accountId: String!): [SkillTaxonomySkillGQL!]! + + # Fetch all available proficiency scales + availableProficiencyScales( + accountId: String! + ): [SkillTaxonomyProficiencyScaleGQL!]! + + # Fetch the currently used proficiency scale for the account + currentlyUsedProficiencyScale: SkillTaxonomyProficiencyScaleGQL + skillTaxonomyGroups(accountId: String!): [SkillTaxonomyGroupGQL!]! + skillTaxonomyCourses(courseIds: [String!]!): [SkillTaxonomyCourseGQL!]! + skillTaxonomyCourse(courseId: String!): SkillTaxonomyCourseGQL! + + # Check if skill extraction has run for course/module/module item + hasSkillExtractionRun(input: SkillExtractionStatusInputGQL!): Boolean! + contentLibraries(accountId: Float!): [ContentLibraryGQL!]! + contentLibrary(courseId: Float!): ContentLibraryGQL! + contentVersions( + assetId: Float + assetType: AssetTypeEnum! + courseId: Float! + ): [ContentVersionGQL!]! + currentContentVersions( + assetType: AssetTypeEnum! + courseId: Float! + ): [ContentVersionGQL!]! + previewContentVersion(id: ID!): ContentVersionPreviewGQL! + assetLinks( + assetId: Float! + assetType: AssetTypeEnum! + contentLibraryCourseId: Float! + ): [AssetLinkGQL!]! + + # All programs available for the user to administer, optionally filtered by progress state + programs( + # Filter programs by progress state + filter: ProgramFilterInput + ): [ProgramGQL!]! + + # A program by id + program(programId: String!): ProgramGQL! + + # All user enrolled programs + enrolledPrograms: [ProgramGQL!]! + crmConfigJwt: CrmJwtGQL! + widgetFileUrl(key: String!): String! + widgetData( + widgetType: String! + dataScope: String! + timeSpan: TimeSpanInput! + canvasAccountId: String + queryParams: WidgetDataFiltersInput + ): WidgetDataResultGQL! + widgetsByUser(userAccountId: String!): [WidgetGQL!]! + widgetByTypeAndUser(userAccountId: String!, type: String!): WidgetGQL + getAthenaQuery(queryExecutionId: String!): JSON! + testAthenaResult(key: String!): [JSON!]! + listS3Keys: [JSON!]! + widgetGroupsByUser(userAccountId: String!): [WidgetGroupGQL!]! + widgetGroupById(widgetGroupId: String!): WidgetGroupGQL! + widgetActions(widgetId: String!): [WidgetActionGQL!]! + + # Get users (and metadata) matching the specified criteria + users(input: UsersQueryInputGQL!): MetadataUsersGql! + + # Get all distinct metadata key-value pairs for a given account + distinctMetadataValues(input: UsersQueryInputGQL!): [UserMetadataGQL!]! + + # Get users (and metadata) reporting to a specified user or the current user + userSubordinates(input: UserReportsQueryInputGQL!): MetadataUsersGql! + + # Get people (Canvas users enriched with metadata) from a specific account + getPeople(input: PeopleQueryInputGQL!): PeopleResponseGQL! + + # Retrieve all insight feed messages for the current user in a specific account. + insights(account: String!): [InsightMessageModel!]! + insightActionLog(account: String!): [InsightActionModel!]! + insightAction(action: String!): InsightActionModel } input SkillExtractionStatusInputGQL { - # Canvas course ID - courseId: String! + # Canvas course ID + courseId: String! - # Optional module ID - moduleId: String + # Optional module ID + moduleId: String - # Optional module item ID - moduleItemId: String + # Optional module item ID + moduleItemId: String } input ProgramFilterInput { - # Filter by program publish status - workflowState: ProgramPublishStatus + # Filter by program publish status + publishStatus: ProgramPublishStatus - # Filter by program progress state - progressState: ProgramProgressState + # Filter by program progress state + progressState: ProgramProgressState } # The publish status of a program enum ProgramPublishStatus { - UNPUBLISHED - PUBLISHED - ARCHIVED - DELETED + UNPUBLISHED + PUBLISHED + ARCHIVED + DELETED } # The progress state of a program enum ProgramProgressState { - NOT_STARTED - IN_PROGRESS - COMPLETED + NOT_STARTED + IN_PROGRESS + COMPLETED } -# The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). -scalar JSON - input TimeSpanInput { - type: TimeSpanType! - startDate: DateTime - endDate: DateTime + type: TimeSpanType! + startDate: DateTime + endDate: DateTime } enum TimeSpanType { - TODAY - PAST_7_DAYS - PAST_30_DAYS - PAST_YEAR - CUSTOM + TODAY + PAST_7_DAYS + PAST_30_DAYS + PAST_YEAR + CUSTOM } input WidgetDataFiltersInput { - userUuids: [String!] + courseIds: [Float!] + courseId: Float + userId: String + userUuids: [String!] + maxLateAssignments: Float + maxMissingAssignments: Float + minLowScorePercentage: Float + delayDaysAfterAction: Float + year: Float + month: Float } input UsersQueryInputGQL { - # UUID of the Canvas root account - rootAccountUuid: ID! + # UUID of the Canvas root account + rootAccountUuid: ID! - # Canvas account ID to check permissions against - accountId: String + # Canvas account ID to check permissions against + accountId: String - # Optional list of Canvas user UUIDs to filter by. If not provided, all users in the root account will be considered. - canvasUserUuids: [ID!] + # Optional list of Canvas user UUIDs to filter by. If not provided, all users in the root account will be considered. + canvasUserUuids: [ID!] - # Optional filters to apply to user metadata. Only users with metadata matching ALL filters will be returned. - metadataFilters: [MetadataFilterInputGQL!] + # Optional filters to apply to user metadata. Only users with metadata matching ALL filters will be returned. + metadataFilters: [MetadataFilterInputGQL!] } input MetadataFilterInputGQL { - # Key to filter metadata by - key: String! + # Key to filter metadata by + key: String! - # Optional value to filter metadata by. If not provided, any value for the key will match. - value: String + # Optional value to filter metadata by. If not provided, any value for the key will match. + value: String } input UserReportsQueryInputGQL { - # UUID of the Canvas root account - rootAccountUuid: ID + # UUID of the Canvas root account + rootAccountUuid: ID - # Canvas user UUID of the user whose reports to retrieve - canvasUserUuid: ID + # Canvas account ID to check permissions against + accountId: String - # Whether to include indirect reports. If false, only direct reports are included. - includeIndirectReports: Boolean! = false + # Canvas user UUID of the user whose reports to retrieve + canvasUserUuid: ID + + # Whether to include indirect reports. If false, only direct reports are included. + includeIndirectReports: Boolean! = false } input PeopleQueryInputGQL { - # The Canvas account ID (sub-account or root account) - accountId: ID! + # The Canvas account ID (sub-account or root account) + accountId: ID! + + # Optional search term to filter users + searchTerm: String - # Optional search term to filter users - searchTerm: String + # Optional flag to return only users who are leaders, will ignore search term if set to true + returnOnlyLeaders: Boolean - # Optional filters to apply to user metadata. Only users with metadata matching ALL filters will be returned. - metadataFilters: [MetadataFilterInputGQL!] + # Optional filters to apply to user metadata. Only users with metadata matching ALL filters will be returned. + metadataFilters: [MetadataFilterInputGQL!] - # Page number for pagination (1-indexed) - page: Int + # Page number for pagination (1-indexed) + page: Int } type Mutation { - createSkills(input: CreateSkillsInputGQL!): [SkillGQL!]! - startSkillExtractionTaskForUser: Boolean! - adminDeleteSkills(skillIds: [String!]!): Boolean! - adminDeleteExperiences(experienceIds: [String!]!): Boolean! - adminRemoveUser(userId: String!): Boolean! - - # Change the skill taxonomy for the given account - changeTaxonomySkills( - input: ChangeTaxonomySkillsInputGQL! - ): ChangeTaxonomySkillsResultGQL! - associateSkillsToLearningObjects( - input: AssociateSkillsToLearningObjectsInputGQL! - ): AssociateSkillsToLearningObjectsResultGQL! - upsertSkillTaxonomyGroups( - input: UpsertSkillTaxonomyGroupsInputGQL! - ): [SkillTaxonomyGroupGQL!]! - sendEvent(input: EventInputGQL!): Boolean! - createContentLibrary(accountId: Float!): ContentLibraryGQL! - updateContentLibrary(args: ContentLibraryInputGQL!): ContentLibraryGQL! - createContentVersion( - assetId: Float! - assetType: AssetTypeEnum! - title: String! - courseId: Float! - syncSettings: SyncSettingsInputGQL! - ): ContentVersionGQL! - importContentVersion( - courseId: Float! - contentVersionId: ID! - moduleId: Float! - detached: Boolean! - ): Float! - - # Create a new program - createProgram(program: String): ProgramGQL! - - # Update program metadata - updateProgramMetadata(metadata: UpdateProgramMetadataGQL!): ProgramGQL! - - # Delete a program - deleteProgram(programId: String!): Boolean! - - # Duplicate an existing program - duplicateProgram(programId: String!): ProgramGQL! - - # Unenroll a Journey user from a program - unenrollUserFromProgram(enrollmentId: String!, canvasUUID: String!): Boolean! - - # Enroll multiple Journey users to a program - batchEnrollUserToProgram( - programId: String! - userIds: [String!]! - ): [ProgramEnrollmentGQL!]! - - # Enroll multiple Journey users to multiple programs - batchEnrollUsersToPrograms( - programIds: [String!]! - enrolleeIds: [String!]! - ): [ProgramEnrollmentGQL!]! - - # Override program requirements - updateProgramRequirements( - programId: String! - requirements: [UpdateProgramRequirementsRequirementGQL!]! - ): [ProgramRequirementGQL!]! - - # Enroll user to program requirement - enroll(progressId: String!): ProgramProgressGQL! - - # Update learner program progresses. This will force recalculation of all progresses for the learner. It can be filtered by Canvas course ID and URL. - updateSelfProgresses( - canvasCourseFilter: CanvasCourseFilterInputGQL - ): ForceProgressRecalcResultGQL! - deleteWidget(id: String!, userAccountId: String!): Boolean! - createWidget(input: CreateWidgetInput!, userAccountId: String!): WidgetGQL! - updateWidget( - id: String! - userAccountId: String! - input: UpdateWidgetInput! - ): WidgetGQL! - testAthenaQuery: String! - runSnapshot: Boolean! - createWidgetGroup(input: CreateWidgetGroupInput!): WidgetGroupGQL! - deleteWidgetGroup(id: String!): Boolean! - updateWidgetGroup( - id: String! - userAccountId: String! - input: UpdateWidgetGroupInput! - ): WidgetGroupGQL! - - # Bulk upsert users and metadata - bulkUpsertMetadata(input: BulkUpsertMetadataInputGQL!): MetadataUsersGql! - - # - # Answer a prompt using the Cedar AI service - # - answerPrompt(prompt: String!): String! - - # Mark an insight feed message as read. - markInsightAsRead(insight: String!): Boolean! - - # Mark an insight feed message as dismissed. - dismissInsights(insights: [String!]!): Boolean! + createSkills(input: CreateSkillsInputGQL!): [SkillGQL!]! + startSkillExtractionTaskForUser: Boolean! + adminDeleteSkills(skillIds: [String!]!): Boolean! + adminDeleteExperiences(experienceIds: [String!]!): Boolean! + adminRemoveUser(userId: String!): Boolean! + + # Change the skill taxonomy for the given account + changeTaxonomySkills( + input: ChangeTaxonomySkillsInputGQL! + ): ChangeTaxonomySkillsResultGQL! + associateSkillsToLearningObjects( + input: AssociateSkillsToLearningObjectsInputGQL! + ): AssociateSkillsToLearningObjectsResultGQL! + upsertSkillTaxonomyGroups( + input: UpsertSkillTaxonomyGroupsInputGQL! + ): [SkillTaxonomyGroupGQL!]! + sendEvent(input: EventInputGQL!): Boolean! + createContentLibrary(accountId: Float!): ContentLibraryGQL! + updateContentLibrary(args: ContentLibraryInputGQL!): ContentLibraryGQL! + createContentVersion( + assetId: Float! + assetType: AssetTypeEnum! + title: String! + courseId: Float! + syncSettings: SyncSettingsInputGQL! + ): ContentVersionGQL! + importContentVersion( + courseId: Float! + contentVersionId: ID! + moduleId: Float! + detached: Boolean! + ): Float! + + # Create a new program + createProgram(program: String): ProgramGQL! + + # Update program metadata + updateProgramMetadata(metadata: UpdateProgramMetadataGQL!): ProgramGQL! + + # Delete a program + deleteProgram(programId: String!): Boolean! + + # Duplicate an existing program + duplicateProgram(programId: String!): ProgramGQL! + + # Unenroll a Journey user from a program + unenrollUserFromProgram(enrollmentId: String!, canvasUUID: String!): Boolean! + + # Enroll multiple Journey users to a program + batchEnrollUserToProgram( + programId: String! + userIds: [String!]! + ): [ProgramEnrollmentGQL!]! + + # Enroll multiple Journey users to multiple programs + batchEnrollUsersToPrograms( + programIds: [String!]! + enrolleeIds: [String!]! + ): [ProgramEnrollmentGQL!]! + + # Override program requirements + updateProgramRequirements( + programId: String! + requirements: [UpdateProgramRequirementsRequirementGQL!]! + ): [ProgramRequirementGQL!]! + + # Enroll user to program requirement + enroll(progressId: String!): ProgramProgressGQL! + + # Update learner program progresses. This will force recalculation of all progresses for the learner. It can be filtered by Canvas course ID and URL. + updateSelfProgresses( + canvasCourseFilter: CanvasCourseFilterInputGQL + ): ForceProgressRecalcResultGQL! + deleteWidget(id: String!, userAccountId: String!): Boolean! + createWidget(input: CreateWidgetInput!, userAccountId: String!): WidgetGQL! + updateWidget( + id: String! + userAccountId: String! + input: UpdateWidgetInput! + ): WidgetGQL! + testAthenaQuery: String! + runSnapshot: Boolean! + createWidgetGroup(input: CreateWidgetGroupInput!): WidgetGroupGQL! + deleteWidgetGroup(id: String!, userAccountId: String!): Boolean! + updateWidgetGroup( + id: String! + userAccountId: String! + input: UpdateWidgetGroupInput! + ): WidgetGroupGQL! + createWidgetAction( + widgetId: String! + userAccountId: String! + inputs: [CreateWidgetActionInput!]! + ): [WidgetActionGQL!]! + + # Bulk upsert users and metadata + bulkUpsertMetadata(input: BulkUpsertMetadataInputGQL!): MetadataUsersGql! + + # + # Answer a prompt using the Cedar AI service + # + answerPrompt(prompt: String!): String! + + # Mark an insight feed message as read. + markInsightAsRead(insight: String!): Boolean! + + # Mark an insight feed message as dismissed. + dismissInsights(insights: [String!]!): Boolean! + + # + # Encourage learners who aren't engaging enough with the platform by sending + # them a motivational email. Returns a handle to the InsightAction. + # + encourageLearners( + insight: String! + subject: String! + body: String! + learners: [String!]! + sendIndividualMessages: Boolean! + ): String! + + # + # Remind learners to begin a course they were enrolled in by sending them + # an email. Returns a handle to the InsightAction. + # + remindLearners( + insight: String! + subject: String! + body: String! + course: String! + learners: [String!]! + sendIndividualMessages: Boolean! + ): String! + + # + # Recognize top-skill achievers by sending them a motivational message to + # their Canvas Inbox. Returns a handle to the InsightAction. + # + praiseLearners( + insight: String! + subject: String! + body: String! + learners: [String!]! + sendIndividualMessages: Boolean! + ): String! } input CreateSkillsInputGQL { - skills: [CreateSkillInputGQL!]! - skillExtractionId: String + skills: [CreateSkillInputGQL!]! + skillExtractionId: String } input CreateSkillInputGQL { - name: String! + name: String! } input ChangeTaxonomySkillsInputGQL { - # Canvas account ID - accountId: String! + # Canvas account ID + accountId: String! - # List of skills to be created/updated in the taxonomy - skillsToPersist: [CreateOrUpdateTaxonomySkillInputGQL!]! + # List of skills to be created/updated in the taxonomy + skillsToPersist: [CreateOrUpdateTaxonomySkillInputGQL!]! - # List of skill IDs to be removed from the taxonomy - skillsToDelete: [ID!]! + # List of skill IDs to be removed from the taxonomy + skillsToDelete: [ID!]! - # List of skills to be merged into a single skill - merges: [MergeSkillsInputGQL!]! + # List of skills to be merged into a single skill + merges: [MergeSkillsInputGQL!]! + + # If true, bypasses semantic similarity checks and persists immediately + skipSimilarityCheck: Boolean! = false } input CreateOrUpdateTaxonomySkillInputGQL { - # Unique identifier for the skill - id: ID + # Unique identifier for the skill + id: ID - # Name of the skill - name: String! + # Name of the skill + name: String! - # Description of the skill - description: String! + # Description of the skill + description: String! - # Group ID to which the skill belongs - groupId: String + # Group ID to which the skill belongs + groupId: String } input MergeSkillsInputGQL { - # Resulting skill after merging - mergedSkill: CreateOrUpdateTaxonomySkillInputGQL! + # Resulting skill after merging + mergedSkill: CreateOrUpdateTaxonomySkillInputGQL! - # List of skill IDs to be merged into the resulting skill - sourceSkillIds: [ID!]! + # List of skill IDs to be merged into the resulting skill + sourceSkillIds: [ID!]! } input AssociateSkillsToLearningObjectsInputGQL { - # Unique identifier for the course - courseId: String! - learningObjects: [CreateOrUpdateAlignmentLearningObjectInputGQL!]! + # Unique identifier for the course + courseId: String! + learningObjects: [CreateOrUpdateAlignmentLearningObjectInputGQL!]! - # List of skill alignment IDs to delete - alignmentsToDelete: [ID!]! + # List of skill alignment IDs to delete + alignmentsToDelete: [ID!]! } input CreateOrUpdateAlignmentLearningObjectInputGQL { - # Unique identifier for the learning object - id: ID + # Unique identifier for the learning object + id: ID - # Unique identifier of the content behind the module item - contentId: String! + # Unique identifier of the content behind the module item + contentId: String! - # Type of the learning object - type: String! + # Type of the learning object + type: String! - # List of skill alignments to create or update for the learning object - alignmentsToPersist: [CreateOrUpdateSkillTaxonomyAlignmentInputGQL!]! + # List of skill alignments to create or update for the learning object + alignmentsToPersist: [CreateOrUpdateSkillTaxonomyAlignmentInputGQL!]! } input CreateOrUpdateSkillTaxonomyAlignmentInputGQL { - # Unique identifier for the alignment - id: ID + # Unique identifier for the alignment + id: ID - # Unique identifier of the module - moduleId: String! + # Unique identifier of the module + moduleId: String! - # Unique identifier of the module item - moduleItemId: String! + # Unique identifier of the module item + moduleItemId: String! - # Unique identifier for the skill - skillId: ID! + # Unique identifier for the skill + skillId: ID! - # Unique identifiers for the proficiency scale values - proficiencyScaleValues: [ID!]! + # Unique identifiers for the proficiency scale values + proficiencyScaleValues: [ID!]! } input UpsertSkillTaxonomyGroupsInputGQL { - # Canvas account ID - accountId: String! + # Canvas account ID + accountId: String! - # List of skill taxonomy groups to be upserted - groups: [UpsertSkillTaxonomyGroupInputGQL!]! + # List of skill taxonomy groups to be upserted + groups: [UpsertSkillTaxonomyGroupInputGQL!]! } input UpsertSkillTaxonomyGroupInputGQL { - # The unique identifier of the group, if updating an existing one - id: String + # The unique identifier of the group, if updating an existing one + id: String - # The name of the skill taxonomy group - name: String! + # The name of the skill taxonomy group + name: String! } input EventInputGQL { - eventType: String! - courseId: String - learningObject: EventLearningObjectInputGQL - learningObjectType: String - moduleId: String - moduleItemId: String - accountId: String + eventType: String! + courseId: String + learningObject: EventLearningObjectInputGQL + learningObjectType: String + moduleId: String + moduleItemId: String + accountId: String } input EventLearningObjectInputGQL { - id: String! - type: String! + id: String! + type: String! } input ContentLibraryInputGQL { - id: String! - externalLinksModuleId: Float - externalToolsModuleId: Float - courseId: Float + id: String! + externalLinksModuleId: Float + externalToolsModuleId: Float + courseId: Float } input SyncSettingsInputGQL { - unpublished: Boolean - published: Boolean - notStarted: Boolean - inProgress: Boolean - completed: Boolean - concluded: Boolean + unpublished: Boolean + published: Boolean + notStarted: Boolean + inProgress: Boolean + completed: Boolean + concluded: Boolean } input UpdateProgramMetadataGQL { - id: String! - name: String! - publicName: String - customerId: String - description: String - publishStatus: String - startDate: DateTime - endDate: DateTime - variant: String - courseCompletionCount: Int - courseEnrollment: CourseEnrollmentType + id: String! + name: String! + publicName: String + customerId: String + description: String + publishStatus: String + startDate: DateTime + endDate: DateTime + variant: String + courseCompletionCount: Int + courseEnrollment: CourseEnrollmentType } input UpdateProgramRequirementsRequirementGQL { - dependency: ProgramRequirementCourseInputGQL - dependent: ProgramRequirementCourseInputGQL! - isCompletionRequired: Boolean! = true - isPlaceholder: Boolean! = false - description: String + dependency: ProgramRequirementCourseInputGQL + dependent: ProgramRequirementCourseInputGQL! + isCompletionRequired: Boolean! = true + isPlaceholder: Boolean! = false + description: String - # Course enrollment type for this requirement - courseEnrollment: String! = "self-enrollment" + # Course enrollment type for this requirement + courseEnrollment: String! = "self-enrollment" } input ProgramRequirementCourseInputGQL { - canvasCourseId: String! - canvasUrl: String! + canvasCourseId: String! + canvasUrl: String! } input CanvasCourseFilterInputGQL { - canvasCourseId: String! - canvasUrl: String! + canvasCourseId: String! + canvasUrl: String! } input CreateWidgetInput { - name: String! - type: String! - queryParams: String! - queryResults: [String!]! - refreshIntervalMinutes: Float - position: Int - widgetGroupId: String + name: String! + type: String! + queryParams: WidgetParamsInput! + refreshIntervalMinutes: Float + position: Int + widgetGroupId: String +} + +input WidgetParamsInput { + courseIds: [Float!] + courseId: Float + userId: String + userUuids: [String!] + maxLateAssignments: Float + maxMissingAssignments: Float + minLowScorePercentage: Float + delayDaysAfterAction: Float + year: Float + month: Float } input UpdateWidgetInput { - name: String - type: String - queryParams: String - queryResults: [String!] - refreshIntervalMinutes: Float - position: Int - widgetGroupId: String + name: String + type: String + queryParams: WidgetParamsInput + refreshIntervalMinutes: Float + position: Int } input CreateWidgetGroupInput { - name: String! - userAccountId: ID! - description: String - position: Float + name: String! + userAccountId: ID! + description: String + widgets: [CreateWidgetInput!] } input UpdateWidgetGroupInput { - name: String - description: String - position: Float - widgetIds: [ID!] + name: String + description: String + updatedWidgets: [UpdatedWidgetInGroupInput!]! + newWidgets: [CreateWidgetInput!] +} + +input UpdatedWidgetInGroupInput { + id: String! + position: Int! +} + +input CreateWidgetActionInput { + action: String! + canvasUserUuid: String! + courseId: String! + actionReason: String! } input BulkUpsertMetadataInputGQL { - # List of users with their metadata to upsert - users: [MetadataUserInputGQL!]! + # List of users with their metadata to upsert + users: [MetadataUserInputGQL!]! } input MetadataUserInputGQL { - # UUID of the user's Canvas root account - canvasRootAccountUuid: ID! + # UUID of the user's Canvas root account + canvasRootAccountUuid: ID! - # User's Canvas UUID - canvasUserUuid: ID! + # User's Canvas UUID + canvasUserUuid: ID! - # List of user's leaders Canvas UUIDs. Set to null to delete the leader association. - leaderCanvasUserUuids: [ID!] + # List of user's leaders Canvas UUIDs. Set to null to delete the leader association. + leaderCanvasUserUuids: [ID!] - # List of metadata to upsert for this user - metadata: [UserMetadataInputGQL!] + # List of metadata to upsert for this user + metadata: [UserMetadataInputGQL!] } input UserMetadataInputGQL { - # Key for the metadata - key: String! + # Key for the metadata + key: String! - # Value for the metadata. Set to null to delete the metadata entry. - value: String + # Value for the metadata. Set to null to delete the metadata entry. + value: String } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/QLClientConfig.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/QLClientConfig.kt index 4b6f755405..04643be4b3 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/QLClientConfig.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/QLClientConfig.kt @@ -38,9 +38,11 @@ import com.instructure.canvasapi2.utils.toApiString import com.instructure.canvasapi2.utils.toDate import okhttp3.OkHttpClient import java.io.File -import java.util.* +import java.util.Date import java.util.concurrent.TimeUnit +private const val DOMAIN_HEADER_KEY = "Override-Domain" + open class QLClientConfig { /** The GraphQL endpoint. Defaults to "/api/graphql/" */ @@ -54,6 +56,18 @@ open class QLClientConfig { .addInterceptor { chain -> chain.proceed(chain.request().newBuilder().addHeader("GraphQL-Metrics", "true").build()) } + .addInterceptor { chain -> + var request = chain.request() + val domain = request.header(DOMAIN_HEADER_KEY) + if (domain != null) { + val newUrl = (domain + GRAPHQL_ENDPOINT) + request = request.newBuilder() + .url(newUrl) + .removeHeader(DOMAIN_HEADER_KEY) + .build() + } + chain.proceed(request) + } .build() var fetchPolicy: HttpFetchPolicy = HttpFetchPolicy.CacheFirst @@ -126,16 +140,31 @@ open class QLClientConfig { suspend fun ApolloClient.enqueueQuery( query: Query, - forceNetwork: Boolean = false + forceNetwork: Boolean = false, + domain: String? = null ): ApolloResponse { - if (forceNetwork) { - return this.query(query).httpFetchPolicy(HttpFetchPolicy.NetworkOnly).executeV3() + val call = if (forceNetwork) { + this.query(query).httpFetchPolicy(HttpFetchPolicy.NetworkOnly) + } else { + this.query(query) + } + + if (domain != null) { + call.addHttpHeader(DOMAIN_HEADER_KEY, domain) } - return this.query(query).executeV3() + + return call.executeV3() } -suspend fun ApolloClient.enqueueMutation( - mutation: Mutation +suspend fun ApolloClient.enqueueMutation( + mutation: Mutation, + domain: String? = null ): ApolloResponse { - return this.mutation(mutation).executeV3() + val call = this.mutation(mutation) + + if (domain != null) { + call.addHttpHeader(DOMAIN_HEADER_KEY, domain) + } + + return call.executeV3() } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AccountNotificationAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AccountNotificationAPI.kt index a2b95e8aa7..8641d184fa 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AccountNotificationAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AccountNotificationAPI.kt @@ -53,6 +53,12 @@ object AccountNotificationAPI { @DELETE("accounts/self/users/self/account_notifications/{accountNotificationId}") fun deleteAccountNotification(@Path("accountNotificationId") accountNotificationId: Long): Call + @DELETE("accounts/self/users/self/account_notifications/{accountNotificationId}") + suspend fun deleteAccountNotification( + @Path("accountNotificationId") accountNotificationId: Long, + @Tag restParams: RestParams + ): DataResult + @GET("accounts/self/users/self/account_notifications/{accountNotificationId}") fun getAccountNotification(@Path("accountNotificationId") accountNotificationId: Long): Call diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AnnouncementAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AnnouncementAPI.kt index a43cb8623e..9dd97dc24d 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AnnouncementAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AnnouncementAPI.kt @@ -43,8 +43,6 @@ object AnnouncementAPI { @GET("announcements") suspend fun getFirstPageAnnouncements( @Query("context_codes[]") vararg courseCode: String, - @Query("start_date") startDate: String, - @Query("end_date") endDate: String, @Tag params: RestParams ): DataResult> diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt index 12237e9380..87a95648c9 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt @@ -67,7 +67,7 @@ object AssignmentAPI { @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=score_statistics&include[]=submission_history") fun getAssignmentWithHistory(@Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long): Call - @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=score_statistics&include[]=submission_history") + @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=score_statistics&include[]=submission_history&include[]=checkpoints&include[]=discussion_topic&include[]=sub_assignment_submissions") suspend fun getAssignmentWithHistory( @Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long, @@ -77,7 +77,7 @@ object AssignmentAPI { @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=observed_users&include[]=score_statistics&include[]=submission_history") fun getAssignmentIncludeObservees(@Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long): Call - @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=observed_users&include[]=score_statistics&include[]=submission_history") + @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=observed_users&include[]=score_statistics&include[]=submission_history&include[]=checkpoints&include[]=discussion_topic&include[]=sub_assignment_submissions") suspend fun getAssignmentIncludeObservees( @Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long, diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt index 9e2559322b..3077e27a48 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt @@ -75,7 +75,7 @@ object CourseAPI { @get:GET("courses?include[]=term&include[]=syllabus_body&include[]=license&include[]=is_public&include[]=permissions&enrollment_state=active") val firstPageCoursesWithSyllabusWithActiveEnrollment: Call> - @get:GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&include[]=settings&state[]=completed&state[]=available&state[]=unpublished") + @get:GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&include[]=settings&state[]=completed&state[]=available&state[]=unpublished&include[]=tabs") val firstPageCoursesTeacher: Call> @GET("courses/{courseId}?include[]=term&include[]=permissions&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=course_image") @@ -120,7 +120,7 @@ object CourseAPI { @GET("courses?state[]=completed&state[]=available&state[]=unpublished") fun getCoursesByEnrollmentType(@Query("enrollment_type") type: String): Call> - @GET("courses?state[]=completed&state[]=available") + @GET("courses?state[]=completed&state[]=available&include[]=term") suspend fun getCoursesByEnrollmentType(@Query("enrollment_type") type: String, @Tag params: RestParams): DataResult> // TODO: Set up pagination when API is fixed and remove per_page query parameterø diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt index e4e81334f3..eef2d1c8a9 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt @@ -79,7 +79,7 @@ object DiscussionAPI { @GET("{contextType}/{contextId}/discussion_topics?override_assignment_dates=true&include[]=all_dates&include[]=overrides&include[]=sections") fun getFirstPageDiscussionTopicHeaders(@Path("contextType") contextType: String, @Path("contextId") contextId: Long): Call> - @GET("{contextType}/{contextId}/discussion_topics?override_assignment_dates=true&include[]=all_dates&include[]=overrides&include[]=sections") + @GET("{contextType}/{contextId}/discussion_topics?override_assignment_dates=true&include[]=all_dates&include[]=overrides&include[]=sections&include[]=checkpoints") suspend fun getFirstPageDiscussionTopicHeaders(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Tag params: RestParams): DataResult> @GET("{contextType}/{contextId}/discussion_topics/{topicId}?include[]=sections") diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FeaturesAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FeaturesAPI.kt index 372321ef1a..989660cedf 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FeaturesAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FeaturesAPI.kt @@ -21,6 +21,7 @@ package com.instructure.canvasapi2.apis import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasFeatureFlag import com.instructure.canvasapi2.models.EnvironmentSettings import com.instructure.canvasapi2.utils.APIHelper import com.instructure.canvasapi2.utils.DataResult @@ -46,6 +47,9 @@ object FeaturesAPI { @GET("settings/environment") suspend fun getAccountSettingsFeatures(@Tag restParams: RestParams): DataResult + + @GET("courses/{courseId}/features/flags/rce_studio_embed_improvements") + suspend fun getStudioEmbedImprovementsFlag(@Path("courseId") courseId: Long, @Tag params: RestParams): DataResult } fun getEnabledFeaturesForCourse(adapter: RestBuilder, courseId: Long, callback: StatusCallback>, params: RestParams) { diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PlannerAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PlannerAPI.kt index 202e2e8ba2..6b17dcbb2d 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PlannerAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PlannerAPI.kt @@ -33,6 +33,7 @@ object PlannerAPI { @Query("start_date") startDate: String?, @Query("end_date") endDate: String?, @Query(value = "context_codes[]", encoded = true) contextCodes: List, + @Query("filter") filter: String? = null, @Tag restParams: RestParams ): DataResult> diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt index 39df056dd7..9d6e3dba91 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt @@ -24,6 +24,9 @@ object UnreadCountAPI { @GET("users/self/activity_stream/summary?only_active_courses=true") fun getNotificationsCount(): Call> + @GET("users/self/activity_stream/summary?only_active_courses=true") + suspend fun getNotificationsCount(@Tag params: RestParams): DataResult> + @GET("users/self/observer_alerts/unread_count") fun getUnreadAlertCount(@Query("student_id") studentId: Long): Call diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/GetCoursesModule.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/GetCoursesModule.kt index 62bef9cb6a..bed35de236 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/GetCoursesModule.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/GetCoursesModule.kt @@ -17,7 +17,8 @@ package com.instructure.canvasapi2.di.graphql import com.apollographql.apollo.ApolloClient import com.instructure.canvasapi2.di.DefaultApolloClient -import com.instructure.canvasapi2.managers.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManagerImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -29,6 +30,6 @@ class GetCoursesModule { @Provides fun provideGetCoursesManager(@DefaultApolloClient apolloClient: ApolloClient): HorizonGetCoursesManager { - return HorizonGetCoursesManager(apolloClient) + return HorizonGetCoursesManagerImpl(apolloClient) } } \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/HorizonGetCommentsModule.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/HorizonGetCommentsModule.kt index 2bc8d078ca..ab18fbdaff 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/HorizonGetCommentsModule.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/HorizonGetCommentsModule.kt @@ -17,7 +17,7 @@ package com.instructure.canvasapi2.di.graphql import com.apollographql.apollo.ApolloClient import com.instructure.canvasapi2.di.DefaultApolloClient -import com.instructure.canvasapi2.managers.HorizonGetCommentsManager +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCommentsManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/JourneyModule.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/JourneyModule.kt new file mode 100644 index 0000000000..ded9706cb1 --- /dev/null +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/JourneyModule.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.canvasapi2.di.graphql + +import com.apollographql.apollo.ApolloClient +import com.instructure.canvasapi2.di.JourneyApolloClient +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetProgramManagerImpl +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetProgramsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetSkillsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetSkillsManagerImpl +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetWidgetsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetWidgetsManagerImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class JourneyModule { + @Provides + fun provideWidgetsManager( + @JourneyApolloClient journeyClient: ApolloClient, + ): GetWidgetsManager { + return GetWidgetsManagerImpl(journeyClient) + } + + @Provides + fun provideProgramsManager( + @JourneyApolloClient journeyClient: ApolloClient + ): GetProgramsManager { + return GetProgramManagerImpl(journeyClient) + } + + @Provides + fun provideSkillsManager( + @JourneyApolloClient journeyClient: ApolloClient + ): GetSkillsManager { + return GetSkillsManagerImpl(journeyClient) + } + +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/ModuleManagerModule.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/ModuleManagerModule.kt new file mode 100644 index 0000000000..d177e52f36 --- /dev/null +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/graphql/ModuleManagerModule.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.canvasapi2.di.graphql + +import com.apollographql.apollo.ApolloClient +import com.instructure.canvasapi2.di.DefaultApolloClient +import com.instructure.canvasapi2.managers.graphql.ModuleManager +import com.instructure.canvasapi2.managers.graphql.ModuleManagerImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class ModuleManagerModule { + + @Provides + fun provideModuleManager(@DefaultApolloClient apolloClient: ApolloClient): ModuleManager { + return ModuleManagerImpl(apolloClient) + } + +} diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/OAuthManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/OAuthManager.kt index c14f354dee..e3bc6ca538 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/OAuthManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/OAuthManager.kt @@ -50,9 +50,9 @@ object OAuthManager { fun getAuthenticatedSessionAsync(targetUrl: String) = apiAsync { getAuthenticatedSession(targetUrl, it) } - fun getAuthenticatedSession(targetUrl: String, callback: StatusCallback) { + fun getAuthenticatedSession(targetUrl: String, callback: StatusCallback, domain: String? = null) { val adapter = RestBuilder(callback) - val params = RestParams(isForceReadFromNetwork = true) + val params = RestParams(isForceReadFromNetwork = true, domain = domain) Logger.d("targetURL to be authed: $targetUrl") val userId = ApiPrefs.user?.id if (ApiPrefs.isMasquerading && userId != null) { diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/QuizManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/QuizManager.kt index e7e5874d4c..f88e4e3e15 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/QuizManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/QuizManager.kt @@ -20,15 +20,14 @@ import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.apis.QuizAPI import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams -import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Quiz +import com.instructure.canvasapi2.models.QuizSubmissionResponse import com.instructure.canvasapi2.models.postmodels.QuizPostBody import com.instructure.canvasapi2.models.postmodels.QuizPostBodyWrapper import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.ExhaustiveListCallback import com.instructure.canvasapi2.utils.weave.apiAsync -import com.instructure.canvasapi2.utils.weave.awaitApi -import okhttp3.ResponseBody -import java.util.* object QuizManager { @@ -88,7 +87,7 @@ object QuizManager { callback: StatusCallback ) { val adapter = RestBuilder(callback) - val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork, domain = ApiPrefs.overrideDomains[canvasContext.id]) QuizAPI.getDetailedQuiz(canvasContext, quizId, adapter, params, callback) } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginLandingPageTest.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/DifferentiationTagsManager.kt similarity index 56% rename from apps/student/src/androidTest/java/com/instructure/student/ui/LoginLandingPageTest.kt rename to libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/DifferentiationTagsManager.kt index c4e6f156b2..faa25d0c42 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginLandingPageTest.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/DifferentiationTagsManager.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 - present Instructure, Inc. + * Copyright (C) 2025 - present Instructure, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,23 +12,13 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ -package com.instructure.student.ui -import com.instructure.student.ui.utils.StudentTest -import com.instructure.espresso.filters.P1 -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test +package com.instructure.canvasapi2.managers.graphql -@HiltAndroidTest -class LoginLandingPageTest: StudentTest() { +import com.instructure.canvasapi2.DifferentiationTagsQuery - // Runs live; no MockCanvas - @Test - @P1 - override fun displaysPageObjects() { - loginLandingPage.assertPageObjects() - } +interface DifferentiationTagsManager { -} + suspend fun getDifferentiationTags(courseId: Long, forceNetwork: Boolean): DifferentiationTagsQuery.Data? +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/DifferentiationTagsManagerImpl.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/DifferentiationTagsManagerImpl.kt new file mode 100644 index 0000000000..a7e633abf9 --- /dev/null +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/DifferentiationTagsManagerImpl.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.canvasapi2.managers.graphql + +import com.apollographql.apollo.ApolloClient +import com.instructure.canvasapi2.DifferentiationTagsQuery +import com.instructure.canvasapi2.enqueueQuery + +class DifferentiationTagsManagerImpl(private val apolloClient: ApolloClient) : DifferentiationTagsManager { + + override suspend fun getDifferentiationTags(courseId: Long, forceNetwork: Boolean): DifferentiationTagsQuery.Data? { + val query = DifferentiationTagsQuery(courseId.toString()) + val result = apolloClient.enqueueQuery(query, forceNetwork) + return result.data + } +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginSignInPageTest.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/ModuleManager.kt similarity index 50% rename from apps/student/src/androidTest/java/com/instructure/student/ui/LoginSignInPageTest.kt rename to libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/ModuleManager.kt index a09366509d..a540088c21 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginSignInPageTest.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/ModuleManager.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 - present Instructure, Inc. + * Copyright (C) 2025 - present Instructure, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,24 +12,24 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ -package com.instructure.student.ui -import com.instructure.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.enterDomain -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test +package com.instructure.canvasapi2.managers.graphql + +import java.util.Date + +data class ModuleItemCheckpoint( + val dueAt: Date?, + val tag: String, + val pointsPossible: Double +) + +data class ModuleItemWithCheckpoints( + val moduleItemId: String, + val checkpoints: List +) -@HiltAndroidTest -class LoginSignInPageTest: StudentTest() { +interface ModuleManager { - // Runs live; no MockCanvas - @Test - override fun displaysPageObjects() { - loginLandingPage.clickFindMySchoolButton() - enterDomain() - loginFindSchoolPage.clickToolbarNextMenuItem() - loginSignInPage.assertPageObjects() - } -} + suspend fun getModuleItemCheckpoints(courseId: String, forceNetwork: Boolean): List +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/ModuleManagerImpl.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/ModuleManagerImpl.kt new file mode 100644 index 0000000000..9ec0c994c2 --- /dev/null +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/ModuleManagerImpl.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.instructure.canvasapi2.managers.graphql + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.Optional +import com.instructure.canvasapi2.ModuleItemCheckpointsQuery +import com.instructure.canvasapi2.QLClientConfig +import com.instructure.canvasapi2.enqueueQuery + +class ModuleManagerImpl(private val apolloClient: ApolloClient) : ModuleManager { + override suspend fun getModuleItemCheckpoints(courseId: String, forceNetwork: Boolean): List { + var hasNextPage = true + var nextCursor: String? = null + val moduleItemsWithCheckpoints = mutableListOf() + + while (hasNextPage) { + val nextCursorParam = if (nextCursor != null) Optional.present(nextCursor) else Optional.absent() + val query = ModuleItemCheckpointsQuery(courseId, QLClientConfig.GRAPHQL_PAGE_SIZE, nextCursorParam) + val data = apolloClient.enqueueQuery(query, forceNetwork).data + val modulesConnection = data?.course?.modulesConnection + + val newItems = modulesConnection?.edges + ?.flatMap { edge -> + edge?.node?.moduleItems?.mapNotNull { moduleItem -> + val discussion = moduleItem.content?.onDiscussion + if (discussion != null && !discussion.checkpoints.isNullOrEmpty()) { + val checkpoints = discussion.checkpoints.map { checkpoint -> + ModuleItemCheckpoint( + dueAt = checkpoint.dueAt, + tag = checkpoint.tag, + pointsPossible = checkpoint.pointsPossible + ) + } + ModuleItemWithCheckpoints( + moduleItemId = moduleItem._id, + checkpoints = checkpoints + ) + } else { + null + } + } ?: emptyList() + } ?: emptyList() + + moduleItemsWithCheckpoints.addAll(newItems) + hasNextPage = modulesConnection?.pageInfo?.hasNextPage ?: false + nextCursor = modulesConnection?.pageInfo?.endCursor + } + + return moduleItemsWithCheckpoints + } +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManager.kt index 083c64f9f1..b02119fe92 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManager.kt @@ -19,5 +19,5 @@ import com.instructure.canvasapi2.SubmissionContentQuery interface SubmissionContentManager { - suspend fun getSubmissionContent(userId: Long, assignmentId: Long): SubmissionContentQuery.Data + suspend fun getSubmissionContent(userId: Long, assignmentId: Long, domain: String? = null): SubmissionContentQuery.Data } \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManagerImpl.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManagerImpl.kt index e147af7997..4071a64367 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManagerImpl.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionContentManagerImpl.kt @@ -25,7 +25,8 @@ class SubmissionContentManagerImpl(private val apolloClient: ApolloClient) : Sub override suspend fun getSubmissionContent( userId: Long, - assignmentId: Long + assignmentId: Long, + domain: String? ): SubmissionContentQuery.Data { var hasNextPage = true var nextCursor: String? = null @@ -41,7 +42,7 @@ class SubmissionContentManagerImpl(private val apolloClient: ApolloClient) : Sub nextCursor = if (nextCursor != null) Optional.present(nextCursor) else Optional.absent() ) - data = apolloClient.enqueueQuery(query, forceNetwork = true).dataAssertNoErrors + data = apolloClient.enqueueQuery(query, forceNetwork = true, domain = domain).dataAssertNoErrors val connection = data.submission?.submissionHistoriesConnection allEdges.addAll(connection?.edges.orEmpty()) diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManager.kt index d1a678d165..14e0f0da88 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManager.kt @@ -20,7 +20,12 @@ import com.instructure.canvasapi2.UpdateSubmissionStatusMutation interface SubmissionGradeManager { - suspend fun getSubmissionGrade(assignmentId: Long, studentId: Long, forceNetwork: Boolean = false): SubmissionGradeQuery.Data + suspend fun getSubmissionGrade( + assignmentId: Long, + studentId: Long, + forceNetwork: Boolean, + domain: String? = null + ): SubmissionGradeQuery.Data suspend fun updateSubmissionGrade(score: Double, submissionId: Long): UpdateSubmissionGradeMutation.Data diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManagerImpl.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManagerImpl.kt index 5d53d9ca2b..7454156a16 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManagerImpl.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/SubmissionGradeManagerImpl.kt @@ -28,7 +28,8 @@ class SubmissionGradeManagerImpl(private val apolloClient: ApolloClient) : Submi override suspend fun getSubmissionGrade( assignmentId: Long, studentId: Long, - forceNetwork: Boolean + forceNetwork: Boolean, + domain: String? ): SubmissionGradeQuery.Data { var hasNextPage = true var nextCursor: String? = null @@ -36,11 +37,9 @@ class SubmissionGradeManagerImpl(private val apolloClient: ApolloClient) : Submi var submission: SubmissionGradeQuery.Data? = null while (hasNextPage) { - val nextCursorParam = - if (nextCursor != null) Optional.present(nextCursor) else Optional.absent() - val query = - SubmissionGradeQuery(studentId.toString(), assignmentId.toString(), nextCursorParam) - val data = apolloClient.enqueueQuery(query, forceNetwork = true).dataAssertNoErrors + val nextCursorParam = if (nextCursor != null) Optional.present(nextCursor) else Optional.absent() + val query = SubmissionGradeQuery(studentId.toString(), assignmentId.toString(), nextCursorParam) + val data = apolloClient.enqueueQuery(query, forceNetwork = true, domain = domain).dataAssertNoErrors if (submission == null) { submission = data } else { diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/DomainServicesAuthenticationManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/DomainServicesAuthenticationManager.kt similarity index 77% rename from libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/DomainServicesAuthenticationManager.kt rename to libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/DomainServicesAuthenticationManager.kt index 7200a9b7e1..d330b419bb 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/DomainServicesAuthenticationManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/DomainServicesAuthenticationManager.kt @@ -1,20 +1,4 @@ -/* - * Copyright (C) 2025 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.instructure.canvasapi2.managers +package com.instructure.canvasapi2.managers.graphql.horizon import com.auth0.jwt.JWT import com.instructure.canvasapi2.apis.DomainServicesAuthenticationAPI @@ -54,12 +38,13 @@ abstract class DomainServicesAuthenticationManager( val workflow = domainService.workflow val params = RestParams() val newToken = domainServicesAuthenticationAPI - .getDomainServiceAuthentication(null, false, DomainServicesWorkflow(listOf(workflow)), params) + .getDomainServiceAuthentication(null, false, + DomainServicesWorkflow(listOf(workflow)), params) .map { it.token } .dataOrNull .orEmpty() - return String(Base64.decode(newToken)) + return String(Base64.Default.decode(newToken)) } private fun isTokenExpired(token: String?): Boolean { diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/HorizonGetCommentsManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/HorizonGetCommentsManager.kt similarity index 98% rename from libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/HorizonGetCommentsManager.kt rename to libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/HorizonGetCommentsManager.kt index ff85d89ee6..086f4e2b73 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/HorizonGetCommentsManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/HorizonGetCommentsManager.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvasapi2.managers +package com.instructure.canvasapi2.managers.graphql.horizon import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.api.Optional diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/HorizonGetCoursesManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/HorizonGetCoursesManager.kt similarity index 62% rename from libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/HorizonGetCoursesManager.kt rename to libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/HorizonGetCoursesManager.kt index ba3ddf3e50..799805839d 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/HorizonGetCoursesManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/HorizonGetCoursesManager.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.canvasapi2.managers +package com.instructure.canvasapi2.managers.graphql.horizon import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.api.Optional @@ -26,9 +26,17 @@ import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.Failure import java.util.Date -class HorizonGetCoursesManager(private val apolloClient: ApolloClient) { +interface HorizonGetCoursesManager { + suspend fun getCoursesWithProgress(userId: Long, forceNetwork: Boolean = false): DataResult> - suspend fun getCoursesWithProgress(userId: Long, forceNetwork: Boolean): DataResult> { + suspend fun getEnrollments(userId: Long, forceNetwork: Boolean = false): DataResult> + + suspend fun getProgramCourses(courseId: Long, forceNetwork: Boolean = false): DataResult +} + +class HorizonGetCoursesManagerImpl(private val apolloClient: ApolloClient): HorizonGetCoursesManager { + + override suspend fun getCoursesWithProgress(userId: Long, forceNetwork: Boolean): DataResult> { return try { val query = GetCoursesQuery(userId.toString()) val result = apolloClient.enqueueQuery(query, forceNetwork).dataAssertNoErrors @@ -55,72 +63,18 @@ class HorizonGetCoursesManager(private val apolloClient: ApolloClient) { } } - suspend fun getDashboardContent(userId: Long, forceNetwork: Boolean): DataResult { + override suspend fun getEnrollments(userId: Long, forceNetwork: Boolean): DataResult> { return try { val query = GetCoursesQuery(userId.toString()) val result = apolloClient.enqueueQuery(query, forceNetwork).dataAssertNoErrors - val coursesList = result.legacyNode?.onUser?.enrollments - ?.filter { it.state == EnrollmentWorkflowState.active } - ?.mapNotNull { mapDashboardCourse(it.course) } ?: emptyList() - val invites = result.legacyNode?.onUser?.enrollments - ?.filter { it.state == EnrollmentWorkflowState.invited } - ?.mapNotNull { mapInvites(it.course, it.id) } ?: emptyList() - return DataResult.Success(DashboardContent(coursesList, invites)) + return DataResult.Success(result.legacyNode?.onUser?.enrollments.orEmpty()) } catch (e: Exception) { DataResult.Fail(Failure.Exception(e)) } } - private fun mapDashboardCourse(course: GetCoursesQuery.Course?): DashboardCourse? { - val courseWithProgress = mapCourse(course) - val institutionName = course?.account?.name - val incompleteModulesConnection = - course?.usersConnection?.nodes?.firstOrNull()?.courseProgression?.incompleteModulesConnection?.nodes?.firstOrNull() - val nextModuleId = incompleteModulesConnection?.module?.id?.toLong() - val nextModuleItemId = incompleteModulesConnection?.incompleteItemsConnection?.nodes?.firstOrNull()?.id?.toLong() - - val nextModuleTitle = incompleteModulesConnection?.module?.name - - val nextModuleItemEstimatedDuration = - incompleteModulesConnection?.incompleteItemsConnection?.nodes?.firstOrNull()?.estimatedDuration - val nextModuleItemDueDate = - incompleteModulesConnection?.incompleteItemsConnection?.nodes?.firstOrNull()?.content?.onAssignment?.dueAt - val nextModuleItemType = incompleteModulesConnection?.incompleteItemsConnection?.nodes?.firstOrNull()?.content?.__typename - val nextModuleItemTitle = incompleteModulesConnection?.incompleteItemsConnection?.nodes?.firstOrNull()?.content?.title - val isNewQuiz = - incompleteModulesConnection?.incompleteItemsConnection?.nodes?.firstOrNull()?.content?.onAssignment?.isNewQuiz ?: false - - return if (courseWithProgress != null) { - DashboardCourse( - courseWithProgress, - institutionName, - nextModuleItemId, - nextModuleId, - nextModuleTitle, - nextModuleItemTitle, - nextModuleItemType, - nextModuleItemDueDate, - nextModuleItemEstimatedDuration, - isNewQuiz - ) - } else { - null - } - } - - private fun mapInvites(course: GetCoursesQuery.Course?, enrollmentId: String?): CourseInvite? { - val courseId = course?.id?.toLong() - val courseName = course?.name - - return if (courseId != null && courseName != null) { - CourseInvite(courseId, courseName, enrollmentId?.toLong() ?: -1L) - } else { - null - } - } - - suspend fun getProgramCourses(courseId: Long, forceNetwork: Boolean = false): DataResult { + override suspend fun getProgramCourses(courseId: Long, forceNetwork: Boolean): DataResult { var hasNextPage = true var nextCursor: String? = null val moduleItemDurations = mutableListOf() diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/CedarApiManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/cedar/CedarApiManager.kt similarity index 85% rename from libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/CedarApiManager.kt rename to libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/cedar/CedarApiManager.kt index ebbac79dc8..a7bdf52c25 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/CedarApiManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/cedar/CedarApiManager.kt @@ -1,20 +1,4 @@ -/* - * Copyright (C) 2025 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.instructure.canvasapi2.managers +package com.instructure.canvasapi2.managers.graphql.horizon.cedar import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.api.Optional @@ -96,7 +80,12 @@ class CedarApiManager @Inject constructor( maxLengthOfQuestions: Int = 100 ): List { val mutation = GenerateQuizMutation( - QuizInput(context, numberOfQuestions.toDouble(), numberOfOptionsPerQuestion.toDouble(), maxLengthOfQuestions.toDouble()) + QuizInput( + context, + numberOfQuestions.toDouble(), + numberOfOptionsPerQuestion.toDouble(), + maxLengthOfQuestions.toDouble() + ) ) val result = cedarClient .enqueueMutation(mutation) diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/JourneyApiManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/journey/GetProgramsManager.kt similarity index 80% rename from libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/JourneyApiManager.kt rename to libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/journey/GetProgramsManager.kt index 25286a3eab..7320612883 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/JourneyApiManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/journey/GetProgramsManager.kt @@ -1,19 +1,20 @@ /* * Copyright (C) 2025 - present Instructure, Inc. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ -package com.instructure.canvasapi2.managers.graphql +package com.instructure.canvasapi2.managers.graphql.horizon.journey import com.apollographql.apollo.ApolloClient import com.instructure.canvasapi2.di.JourneyApolloClient @@ -50,10 +51,16 @@ data class ProgramRequirement( val enrollmentStatus: ProgramProgressCourseEnrollmentStatus? = null ) -class JourneyApiManager @Inject constructor( - @JourneyApolloClient private val journeyClient: ApolloClient -) { - suspend fun getPrograms(forceNetwork: Boolean): List { +interface GetProgramsManager { + suspend fun getPrograms(forceNetwork: Boolean = false): List + suspend fun getProgramById(programId: String, forceNetwork: Boolean = false): Program + suspend fun enrollCourse(progressId: String): DataResult +} + +class GetProgramManagerImpl @Inject constructor( + @JourneyApolloClient private val journeyClient: ApolloClient, +): GetProgramsManager { + override suspend fun getPrograms(forceNetwork: Boolean): List { val query = EnrolledProgramsQuery() val result = journeyClient.enqueueQuery(query, forceNetwork = forceNetwork) return result.dataAssertNoErrors.enrolledPrograms.map { @@ -61,7 +68,7 @@ class JourneyApiManager @Inject constructor( } } - suspend fun getProgramById(programId: String, forceNetwork: Boolean): Program { + override suspend fun getProgramById(programId: String, forceNetwork: Boolean): Program { val query = GetProgramByIdQuery(programId) val result = journeyClient.enqueueQuery(query, forceNetwork = forceNetwork) return mapEnrolledProgram(result.dataAssertNoErrors.program.programFields) @@ -126,7 +133,7 @@ class JourneyApiManager @Inject constructor( ) } - suspend fun enrollCourse(progressId: String): DataResult { + override suspend fun enrollCourse(progressId: String): DataResult { val mutation = EnrollCourseMutation(progressId) val result = journeyClient.enqueueMutation(mutation) return if (result.exception != null) { diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/journey/GetSkillsManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/journey/GetSkillsManager.kt new file mode 100644 index 0000000000..f4bcc4b26a --- /dev/null +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/journey/GetSkillsManager.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.canvasapi2.managers.graphql.horizon.journey + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.Optional +import com.instructure.canvasapi2.enqueueQuery +import com.instructure.journey.GetSkillsQuery +import java.util.Date +import javax.inject.Inject + +data class Skill( + val id: String, + val name: String, + val proficiencyLevel: String?, + val createdAt: Date?, + val updatedAt: Date? +) + +interface GetSkillsManager { + suspend fun getSkills(completedOnly: Boolean?, forceNetwork: Boolean): List +} + +class GetSkillsManagerImpl @Inject constructor( + private val journeyClient: ApolloClient +) : GetSkillsManager { + override suspend fun getSkills( + completedOnly: Boolean?, + forceNetwork: Boolean + ): List { + val query = GetSkillsQuery( + completedOnly = Optional.presentIfNotNull(completedOnly) + ) + + val result = journeyClient.enqueueQuery(query, forceNetwork) + val skills = result.dataAssertNoErrors.skills + + return skills.map { skill -> + Skill( + id = skill.id, + name = skill.name, + proficiencyLevel = skill.proficiencyLevel, + createdAt = skill.createdAt, + updatedAt = skill.updatedAt + ) + } + } +} diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/journey/GetWidgetsManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/journey/GetWidgetsManager.kt new file mode 100644 index 0000000000..a1bd4cebc7 --- /dev/null +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/journey/GetWidgetsManager.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.canvasapi2.managers.graphql.horizon.journey + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.Optional +import com.instructure.canvasapi2.enqueueQuery +import com.instructure.journey.GetWidgetDataQuery +import com.instructure.journey.type.TimeSpanInput +import com.instructure.journey.type.TimeSpanType +import com.instructure.journey.type.WidgetDataFiltersInput +import javax.inject.Inject + +interface GetWidgetsManager { + suspend fun getTimeSpentWidgetData(courseId: Long?, forceNetwork: Boolean): GetWidgetDataQuery.WidgetData + suspend fun getLearningStatusWidgetData(courseId: Long?, forceNetwork: Boolean): GetWidgetDataQuery.WidgetData +} + +class GetWidgetsManagerImpl @Inject constructor( + private val journeyClient: ApolloClient, +) : GetWidgetsManager { + override suspend fun getTimeSpentWidgetData( + courseId: Long?, + forceNetwork: Boolean, + ): GetWidgetDataQuery.WidgetData { + val widgetType = "time_spent_details" + val timeSpanInput = TimeSpanInput(type = TimeSpanType.PAST_7_DAYS) + val dataScope = "learner" + val queryParams = if (courseId != null) { + Optional.present( + WidgetDataFiltersInput(courseId = Optional.present(courseId.toDouble())) + ) + } else { + Optional.absent() + } + + val query = GetWidgetDataQuery( + widgetType = widgetType, + timeSpan = timeSpanInput, + dataScope = dataScope, + queryParams = queryParams + ) + + val result = journeyClient.enqueueQuery(query, forceNetwork) + return result.dataAssertNoErrors.widgetData + } + + override suspend fun getLearningStatusWidgetData( + courseId: Long?, + forceNetwork: Boolean, + ): GetWidgetDataQuery.WidgetData { + val widgetType = "learning_status_details" + val timeSpanInput = TimeSpanInput(type = TimeSpanType.PAST_7_DAYS) + val dataScope = "learner" + val queryParams = if (courseId != null) { + Optional.present( + WidgetDataFiltersInput(courseId = Optional.present(courseId.toDouble())) + ) + } else { + Optional.absent() + } + + val query = GetWidgetDataQuery( + widgetType = widgetType, + timeSpan = timeSpanInput, + dataScope = dataScope, + queryParams = queryParams + ) + + val result = journeyClient.enqueueQuery(query, forceNetwork) + return result.dataAssertNoErrors.widgetData + } +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/PineApiManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/pine/PineApiManager.kt similarity index 97% rename from libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/PineApiManager.kt rename to libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/pine/PineApiManager.kt index fab27af5c5..843061390b 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/PineApiManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/pine/PineApiManager.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.canvasapi2.managers +package com.instructure.canvasapi2.managers.graphql.horizon.pine import com.apollographql.apollo.ApolloClient import com.instructure.canvasapi2.di.PineApolloClient diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/RedwoodApiManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/redwood/RedwoodApiManager.kt similarity index 98% rename from libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/RedwoodApiManager.kt rename to libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/redwood/RedwoodApiManager.kt index 341e7c2909..c9e168c76c 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/RedwoodApiManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/graphql/horizon/redwood/RedwoodApiManager.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.canvasapi2.managers +package com.instructure.canvasapi2.managers.graphql.horizon.redwood import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.api.Optional diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/AccountNotification.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/AccountNotification.kt index 6b25409fe4..ce10ab62bd 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/AccountNotification.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/AccountNotification.kt @@ -30,7 +30,8 @@ data class AccountNotification( val startAt: String = "", @SerializedName("end_at") val endAt: String = "", - val icon: String = "" + val icon: String = "", + val closed: Boolean = false ) : CanvasModel() { override val comparisonString get() = subject override val comparisonDate get() = startDate diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt index 8e427574ee..468b004053 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt @@ -253,6 +253,10 @@ data class Assignment( } ?: LtiType.EXTERNAL_TOOL } + fun isQuiz(): Boolean { + return getSubmissionTypes().contains(SubmissionType.ONLINE_QUIZ) || ltiToolType() == LtiType.NEW_QUIZZES_LTI + } + companion object { const val PASS_FAIL_TYPE = "pass_fail" diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerItem.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerItem.kt index 57f9b4a226..a68ac52c95 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerItem.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/PlannerItem.kt @@ -17,6 +17,7 @@ package com.instructure.canvasapi2.models import android.os.Parcelable import com.google.gson.annotations.SerializedName +import com.instructure.canvasapi2.utils.toApiString import kotlinx.parcelize.Parcelize import java.util.* @@ -81,6 +82,34 @@ data class PlannerItem ( override val comparisonString: String get() = "${plannable.title}${plannable.subAssignmentTag}" + fun toScheduleItem(): ScheduleItem { + val contextCode = when { + courseId != null -> "course_$courseId" + groupId != null -> "group_$groupId" + userId != null -> "user_$userId" + else -> null + } + + return ScheduleItem( + itemId = plannable.id.toString(), + title = plannable.title, + description = plannable.details, + startAt = plannable.todoDate ?: plannableDate.toApiString(), + endAt = plannable.endAt?.toApiString(), + isAllDay = plannable.allDay ?: false, + allDayAt = if (plannable.allDay == true) plannable.todoDate else null, + htmlUrl = htmlUrl, + contextCode = contextCode, + contextName = contextName, + type = when (plannableType) { + PlannableType.DISCUSSION_TOPIC -> "discussion_topic" + PlannableType.WIKI_PAGE -> "wiki_page" + else -> plannableType.name.lowercase() + }, + itemType = ScheduleItem.Type.TYPE_SYLLABUS + ) + } + } enum class PlannableType { diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt index fd9b471ec7..de6f34fd39 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt @@ -71,11 +71,11 @@ object AnalyticsEventConstants { const val ASSIGNMENT_LIST_SORT_BY_TYPE_SELECTED = "assignment_list_sort_by_type_selected" const val ASSIGNMENT_SUBMIT_SELECTED = "assignment_submit_selected" const val SUBMIT_FILEUPLOAD_SELECTED = "submit_fileupload_selected" - const val SUBMIT_ONLINEURL_SELECTED = "submit_onlineurl_selected" + const val SUBMIT_URL_SELECTED = "submit_url_selected" const val SUBMIT_TEXTENTRY_SELECTED = "submit_textentry_selected" const val SUBMIT_MEDIARECORDING_SELECTED = "submit_mediarecording_selected" const val SUBMIT_STUDIO_SELECTED = "submit_studio_selected" - const val SUBMIT_STUDENT_ANNOTATION_SELECTED = "submit_student_annotation_selected" + const val SUBMIT_ANNOTATION_SELECTED = "submit_annotation_selected" const val SUBMISSION_CELL_SELECTED = "submission_cell_selected" const val SUBMISSION_COMMENTS_SELECTED = "submission_comments_selected" const val SUBMISSION_COMMENTS_TEXT_REPLY = "submission_comments_text_reply" @@ -85,6 +85,17 @@ object AnalyticsEventConstants { const val SUBMISSION_ANNOTATION_SELECTED = "submission_annotation_selected" const val SUBMIT_FILEUPLOAD_FAILED = "submit_fileupload_failed" const val SUBMIT_FILEUPLOAD_SUCCEEDED = "submit_fileupload_succeeded" + const val SUBMIT_TEXTENTRY_SUCCEEDED = "submit_textentry_succeeded" + const val SUBMIT_TEXTENTRY_FAILED = "submit_textentry_failed" + const val SUBMIT_URL_SUCCEEDED = "submit_url_succeeded" + const val SUBMIT_URL_FAILED = "submit_url_failed" + const val SUBMIT_ANNOTATION_PRESENTED = "submit_annotation_presented" + const val SUBMIT_ANNOTATION_SUCCEEDED = "submit_annotation_succeeded" + const val SUBMIT_ANNOTATION_FAILED = "submit_annotation_failed" + const val SUBMIT_STUDIO_SUCCEEDED = "submit_studio_succeeded" + const val SUBMIT_STUDIO_FAILED = "submit_studio_failed" + const val SUBMIT_MEDIARECORDING_SUCCEEDED = "submit_mediarecording_succeeded" + const val SUBMIT_MEDIARECORDING_FAILED = "submit_mediarecording_failed" const val UNSUPPORTED_SUBMISSION_CONTENT = "unsupported_submission_content" /* Panda Avatar */ @@ -176,4 +187,7 @@ object AnalyticsParamConstants { const val MANUAL_C4E_STATE = "manual_c4e_state" const val DURATION = "duration" const val STAR_RATING = "star_rating" + const val MEDIA_SOURCE = "media_source" + const val MEDIA_TYPE = "media_type" + const val ATTEMPT = "attempt" } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/DomainServicesAuthenticator.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/DomainServicesAuthenticator.kt index fba771b271..411e834584 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/DomainServicesAuthenticator.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/DomainServicesAuthenticator.kt @@ -16,11 +16,11 @@ */ package com.instructure.canvasapi2.utils -import com.instructure.canvasapi2.managers.CedarAuthenticationManager -import com.instructure.canvasapi2.managers.DomainServicesAuthenticationManager -import com.instructure.canvasapi2.managers.JourneyAuthenticationManager -import com.instructure.canvasapi2.managers.PineAuthenticationManager -import com.instructure.canvasapi2.managers.RedwoodAuthenticationManager +import com.instructure.canvasapi2.managers.graphql.horizon.CedarAuthenticationManager +import com.instructure.canvasapi2.managers.graphql.horizon.DomainServicesAuthenticationManager +import com.instructure.canvasapi2.managers.graphql.horizon.JourneyAuthenticationManager +import com.instructure.canvasapi2.managers.graphql.horizon.PineAuthenticationManager +import com.instructure.canvasapi2.managers.graphql.horizon.RedwoodAuthenticationManager import kotlinx.coroutines.runBlocking import okhttp3.Authenticator import okhttp3.Request diff --git a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/AssignmentTest.kt b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/AssignmentTest.kt index 81185b1e4c..5ba712a56b 100644 --- a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/AssignmentTest.kt +++ b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/AssignmentTest.kt @@ -290,4 +290,38 @@ class AssignmentTest { } //endregion + //region isQuiz + @Test + fun isQuiz_TestOnlineQuiz() { + val assignment = Assignment(submissionTypesRaw = listOf("online_quiz")) + assertEquals(true, assignment.isQuiz()) + } + + @Test + fun isQuiz_TestQuizLtiAssignment() { + val externalToolAttributes = ExternalToolAttributes(url = "https://example.com/quiz-lti/launch") + val assignment = Assignment( + submissionTypesRaw = listOf("external_tool"), + externalToolAttributes = externalToolAttributes + ) + assertEquals(true, assignment.isQuiz()) + } + + @Test + fun isQuiz_TestRegularExternalTool() { + val externalToolAttributes = ExternalToolAttributes(url = "https://example.com/tool/launch") + val assignment = Assignment( + submissionTypesRaw = listOf("external_tool"), + externalToolAttributes = externalToolAttributes + ) + assertEquals(false, assignment.isQuiz()) + } + + @Test + fun isQuiz_TestNonQuizSubmissionType() { + val assignment = Assignment(submissionTypesRaw = listOf("online_upload")) + assertEquals(false, assignment.isQuiz()) + } + //endregion + } diff --git a/libs/horizon/CLAUDE.md b/libs/horizon/CLAUDE.md new file mode 100644 index 0000000000..eeb23d6f88 --- /dev/null +++ b/libs/horizon/CLAUDE.md @@ -0,0 +1,145 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +Horizon is an Android library module within the canvas-android monorepo that provides a modern, Jetpack Compose-based learning experience for Canvas Career. It is part of the Instructure Canvas LMS mobile ecosystem and integrates with the Student app. The module is located at `libs/horizon` within the monorepo. + +## Build Commands + +All Gradle commands should be run from the repository root (`canvas-android/`), not from the `libs/horizon` directory. + +### Building +```bash +./gradle/gradlew :libs:horizon:assembleDebug +./gradle/gradlew :libs:horizon:assembleRelease +``` + +### Testing +```bash +# Run unit tests (JUnit + Robolectric) +./gradle/gradlew :libs:horizon:testDebugUnitTest + +# Run instrumented tests (Espresso + Compose UI tests) +./gradle/gradlew :libs:horizon:connectedDebugAndroidTest + +# Run specific test class +./gradle/gradlew :libs:horizon:testDebugUnitTest --tests "com.instructure.horizon.features.dashboard.DashboardViewModelTest" +``` + +### Linting and Code Quality +```bash +./gradle/gradlew :libs:horizon:lintDebug +``` + +## Architecture + +### Tech Stack +- **Language**: Kotlin +- **UI**: Jetpack Compose with Material3 +- **Dependency Injection**: Dagger Hilt +- **Navigation**: Jetpack Navigation Compose with type-safe routes using Kotlin Serialization +- **Async**: Kotlin Coroutines and Flow +- **Testing**: JUnit, Mockk, Robolectric, Espresso, Compose UI Test + +### Project Structure + +``` +src/main/java/com/instructure/horizon/ +├── HorizonActivity.kt # Main entry point, handles navigation and push notifications +├── di/ # Hilt dependency injection modules +├── features/ # Feature modules organized by domain +│ ├── account/ +│ ├── aiassistant/ +│ ├── dashboard/ +│ ├── home/ # Landing screen with navigation +│ ├── inbox/ # Messaging functionality +│ ├── learn/ # Course and program learning content +│ ├── moduleitemsequence/ # Sequential module item navigation +│ ├── notebook/ # Note-taking functionality +│ ├── notification/ # Notification center +│ └── skillspace/ +├── horizonui/ # Design system components +│ ├── HorizonTheme.kt # Theme configuration (forced light mode) +│ ├── foundation/ # Colors, typography, spacing +│ ├── molecules/ # Small reusable components +│ ├── organisms/ # Complex composed components +│ └── animation/ # Navigation transitions +├── model/ # Domain models specific to Horizon +└── navigation/ + └── HorizonNavigation.kt # NavHost and routing configuration +``` + +### Design system + + - The `horizonui` package contains the major UI components + - If possible use these components + - If the design contains a reusable component extend the existing design system + - Local Componenents from Figma design shouldn't be placed here, implement them in the feature's package + + +### Feature Architecture Pattern + +Each feature follows MVVM pattern with a consistent structure: + +``` +features/[feature-name]/ +├── [FeatureName]Screen.kt # Composable UI +├── [FeatureName]ViewModel.kt # Hilt-injected ViewModel with StateFlow +├── [FeatureName]Repository.kt # Data layer, API calls +├── [FeatureName]UiState.kt # UI state data class +└── navigation/ # Feature-specific navigation graphs +``` + +**Example flow:** +- `HomeScreen` → `HomeViewModel` → `HomeRepository` → Canvas API +- ViewModels expose `StateFlow` collected in Composables +- Repositories use suspend functions and return API models from `canvasapi2` + +### Key Design Patterns + +1. **Navigation**: Type-safe routes using `@Serializable` data classes in `MainNavigationRoute.kt` +2. **Deep Linking**: Deep links configured in NavHost composables map to Canvas LMS URLs +3. **Dependency Injection**: `@HiltViewModel` for ViewModels, constructor injection for repositories +4. **UI State Management**: Immutable data classes with sealed classes for state variants +5. **Testing**: + - Unit tests use Mockk with `UnconfinedTestDispatcher` for coroutines + - UI tests extend `HorizonTest` base class and use Page Object pattern + +## Important Dependencies + +- `:pandautils` - Core shared utilities, base classes, and common Canvas components +- Canvas API 2 - REST API client for Canvas LMS (`com.instructure.canvasapi2`) +- PSPDFKit - PDF annotation and viewing +- AndroidX WorkManager - Background task scheduling for submissions + +## Testing Notes + +- **Unit Tests**: Located in `src/test/`, use Robolectric for Android framework dependencies +- **Instrumented Tests**: Located in `src/androidTest/`, divided into: + - `espresso/` - Test infrastructure and base classes + - `ui/features/` - UI-only tests using Compose Test + - `interaction/features/` - Full integration tests with data seeding + - Every application screen has a test page which contains all of the important assertions and actions. Create and use these Page classes for every new screen. +- **Test Authentication**: Use `tokenLogin()` method from `HorizonTest` base class for instrumented tests +- **Data Seeding**: Available via `:dataseedingapi` module for instrumented tests +- **Mocked Data**: MockCanvas is an object which can contain all the mock data which will be returned by the mock endpoints during testing + +## Horizon-Specific Considerations + +1. **Theme**: Horizon forces light mode in `HorizonActivity.onCreate()` to ensure consistent branding +2. **Navigation Deep Links**: Must include `ApiPrefs.fullDomain` in URI patterns for proper routing +3. **Module Item Sequence**: Central navigation pattern for course content (assignments, quizzes, pages) +4. **Canvas Career**: This module specifically supports Canvas Career View (`ApiPrefs.canvasCareerView = true`) + +## Code Style + +Follow the code style guidelines from the parent repository: +- No inline comments unless specifically requested - code should be self-documenting +- Use Kotlin idioms and best practices +- Prefer immutability and data classes +- Use descriptive naming for functions and variables +- Write tests that mirror existing test patterns in the project +- Match existing file structure and naming conventions when adding new features +- Alwways create a preview composable for the new composables \ No newline at end of file diff --git a/libs/horizon/build.gradle.kts b/libs/horizon/build.gradle.kts index 6b68c1b589..41d8a6056d 100644 --- a/libs/horizon/build.gradle.kts +++ b/libs/horizon/build.gradle.kts @@ -3,9 +3,10 @@ plugins { id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.plugin.compose") id("kotlin-android") - id("kotlin-kapt") + id("com.google.devtools.ksp") id("dagger.hilt.android.plugin") kotlin("plugin.serialization") version "2.1.20" + id("jacoco") } android { @@ -15,9 +16,8 @@ android { defaultConfig { minSdk = Versions.MIN_SDK - targetSdk = Versions.TARGET_SDK - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner = "com.instructure.horizon.espresso.HorizonCustomTestRunner" consumerProguardFiles("consumer-rules.pro") } @@ -57,6 +57,10 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() } + + hilt { + enableAggregatingTask = false + } } dependencies { @@ -64,11 +68,11 @@ dependencies { implementation(Libs.NAVIGATION_COMPOSE) implementation(Libs.HILT) - kapt(Libs.HILT_COMPILER) + ksp(Libs.HILT_COMPILER) implementation(Libs.HILT_ANDROIDX_WORK) - kapt(Libs.HILT_ANDROIDX_COMPILER) + ksp(Libs.HILT_ANDROIDX_COMPILER) - implementation(Libs.PSPDFKIT) + implementation(Libs.NUTRIENT) implementation(Libs.ANDROIDX_ANNOTATION) implementation(Libs.ANDROIDX_APPCOMPAT) @@ -85,4 +89,85 @@ dependencies { implementation(Libs.FIREBASE_CRASHLYTICS) { isTransitive = true } + + /* Android Test Dependencies */ + androidTestImplementation(project(":espresso")) + androidTestImplementation(project(":dataseedingapi")) + androidTestImplementation(Libs.COMPOSE_UI_TEST) + + /* Unit Test Dependencies */ + testImplementation(Libs.JUNIT) + testImplementation(Libs.ROBOLECTRIC) + testImplementation(Libs.ANDROIDX_TEST_JUNIT) + testImplementation(Libs.MOCKK) + androidTestImplementation(Libs.ANDROIDX_TEST_JUNIT) + testImplementation(Libs.KOTLIN_COROUTINES_TEST) + testImplementation(Libs.THREETEN_BP) + testImplementation(Libs.ANDROIDX_CORE_TESTING) + androidTestImplementation(Libs.HILT_TESTING) + + /* Pandautils dependencies to provide fake implementations for testing */ + androidTestImplementation(Libs.PLAY_IN_APP_UPDATES) + androidTestImplementation(Libs.ROOM) +} + +tasks.register("jacocoTestReport") { + dependsOn("testDebugUnitTest") + + reports { + xml.required.set(true) + html.required.set(true) + csv.required.set(false) + } + + val fileFilter = listOf( + "**/R.class", + "**/R$*.class", + "**/BuildConfig.*", + "**/Manifest*.*", + "**/*Test*.*", + "android/**/*.*", + "**/*\$ViewInjector*.*", + "**/*\$ViewBinder*.*", + "**/Lambda$*.class", + "**/Lambda.class", + "**/*Lambda.class", + "**/*Lambda*.class", + "**/*_MembersInjector.class", + "**/Dagger*Component*.*", + "**/*Module_*Factory.class", + "**/di/module/*", + "**/*_Factory*.*", + "**/*Module*.*", + "**/*Dagger*.*", + "**/*Hilt*.*", + "**/hilt_aggregated_deps/**", + "**/*_HiltModules*.*", + "**/*_ComponentTreeDeps*.*", + "**/*_Impl*.*", + "**/*Screen*.*", + "**/*Ui*.*", + "**/*Navigation*.*", + "**/*Activity*.*", + "**/*Fragment*.*", + "**/*Composable*.*", + "**/*Preview*.*", + "**/horizonui/**", + "**/model/**", + "**/navigation/**" + ) + + val debugTree = fileTree("${layout.buildDirectory.get().asFile}/tmp/kotlin-classes/debug") { + exclude(fileFilter) + include("**/features/**/*ViewModel*.class") + include("**/features/**/*Repository*.class") + } + + val mainSrc = "${project.projectDir}/src/main/java" + + sourceDirectories.setFrom(files(mainSrc)) + classDirectories.setFrom(files(debugTree)) + executionData.setFrom(fileTree(layout.buildDirectory.get().asFile) { + include("jacoco/testDebugUnitTest.exec") + }) } \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonActivityTestRule.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonActivityTestRule.kt new file mode 100644 index 0000000000..c0e78bd6d4 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonActivityTestRule.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.espresso + +import android.app.Activity +import android.content.Context +import com.instructure.espresso.InstructureActivityTestRule + +class HorizonActivityTestRule(activityClass: Class) : InstructureActivityTestRule(activityClass) { + override fun performReset(context: Context) { + + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonCustomTestRunner.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonCustomTestRunner.kt new file mode 100644 index 0000000000..4ed54cefce --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonCustomTestRunner.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.espresso + +import android.app.Application +import android.content.Context +import com.instructure.canvas.espresso.CanvasRunner +import com.jakewharton.threetenabp.AndroidThreeTen +import dagger.hilt.android.testing.HiltTestApplication + +class HorizonCustomTestRunner: CanvasRunner() { + override fun newApplication( + cl: ClassLoader?, + className: String?, + context: Context? + ): Application { + val application = super.newApplication(cl, HiltTestApplication::class.java.name, context) + AndroidThreeTen.init(application) + return application + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonTest.kt new file mode 100644 index 0000000000..b5570bdbdd --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/HorizonTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.espresso + +import com.instructure.canvas.espresso.CanvasComposeTest +import com.instructure.canvasapi2.models.User +import com.instructure.horizon.HorizonActivity +import com.instructure.horizon.pages.HorizonDashboardPage +import com.instructure.horizon.pages.HorizonNotificationPage + +abstract class HorizonTest: CanvasComposeTest() { + override val activityRule = HorizonActivityTestRule(HorizonActivity::class.java) + override val isTesting = true + + override fun displaysPageObjects() = Unit + + val dashboardPage: HorizonDashboardPage = HorizonDashboardPage(composeTestRule) + val notificationsPage: HorizonNotificationPage = HorizonNotificationPage(composeTestRule) + + fun tokenLogin(domain: String, token: String, user: User) { + activityRule.runOnUiThread { + (originalActivity as HorizonActivity).loginWithToken( + token, + domain, + user + ) + } + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/TestModule.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/TestModule.kt new file mode 100644 index 0000000000..9997f8b0a6 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/espresso/TestModule.kt @@ -0,0 +1,301 @@ +package com.instructure.horizon.espresso + +import android.content.Intent +import com.instructure.canvasapi2.LoginRouter +import com.instructure.pandautils.features.offline.sync.SyncRouter +import com.instructure.pandautils.features.speedgrader.content.SpeedGraderContentRouter +import com.instructure.pandautils.features.speedgrader.grade.comments.SpeedGraderCommentsAttachmentRouter +import com.instructure.pandautils.receivers.alarm.AlarmReceiverNotificationHandler +import com.instructure.pandautils.room.appdatabase.AppDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object HorizonTestModule { + @Provides + fun provideSpeedGraderContentRouter(): SpeedGraderContentRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideSpeedGraderCommentsAttachmentRouter(): SpeedGraderCommentsAttachmentRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAlarmReceiverNotificationHandler(): AlarmReceiverNotificationHandler { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideLoginRouter(): LoginRouter { + return object : LoginRouter { + override fun loginIntent(): Intent { + return Intent() + } + } + } + + @Provides + fun provideAppDatabase(): AppDatabase { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideSyncRouter(): SyncRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideLogoutHelper(): com.instructure.pandautils.utils.LogoutHelper { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun providePandataInfoAppKey(): com.instructure.canvasapi2.utils.pageview.PandataInfo.AppKey { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideDiscussionRouteHelperRepository(): com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentDetailsRouter(): com.instructure.pandautils.features.assignments.details.AssignmentDetailsRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideWebViewRouter(): com.instructure.pandautils.navigation.WebViewRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentDetailsBehaviour(): com.instructure.pandautils.features.assignments.details.AssignmentDetailsBehaviour { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentListRouter(): com.instructure.pandautils.features.assignments.list.AssignmentListRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideCalendarRouter(): com.instructure.pandautils.features.calendar.CalendarRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideEventRouter(): com.instructure.pandautils.features.calendarevent.details.EventRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideToDoRouter(): com.instructure.pandautils.features.calendartodo.details.ToDoRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideEditDashboardRouter(): com.instructure.pandautils.features.dashboard.edit.EditDashboardRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideShareExtensionRouter(): com.instructure.pandautils.features.shareextension.ShareExtensionRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideDashboardRouter(): com.instructure.pandautils.features.dashboard.notifications.DashboardRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideDiscussionRouter(): com.instructure.pandautils.features.discussion.router.DiscussionRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideDiscussionDetailsWebViewFragmentBehavior(): com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragmentBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideGradesRouter(): com.instructure.pandautils.features.elementary.grades.GradesRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideHomeroomRouter(): com.instructure.pandautils.features.elementary.homeroom.HomeroomRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideImportantDatesRouter(): com.instructure.pandautils.features.elementary.importantdates.ImportantDatesRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideResourcesRouter(): com.instructure.pandautils.features.elementary.resources.itemviewmodels.ResourcesRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideScheduleRouter(): com.instructure.pandautils.features.elementary.schedule.ScheduleRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideHelpDialogFragmentBehavior(): com.instructure.pandautils.features.help.HelpDialogFragmentBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideInboxRouter(): com.instructure.pandautils.features.inbox.list.InboxRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideLegalRouter(): com.instructure.pandautils.features.legal.LegalRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideLtiLaunchFragmentBehavior(): com.instructure.pandautils.features.lti.LtiLaunchFragmentBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideSettingsRouter(): com.instructure.pandautils.features.settings.SettingsRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideSmartSearchRouter(): com.instructure.pandautils.features.smartsearch.SmartSearchRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAboutRepository(): com.instructure.pandautils.features.about.AboutRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentDetailsRepository(): com.instructure.pandautils.features.assignments.details.AssignmentDetailsRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentDetailsSubmissionHandler(): com.instructure.pandautils.features.assignments.details.AssignmentDetailsSubmissionHandler { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentDetailsColorProvider(): com.instructure.pandautils.features.assignments.details.AssignmentDetailsColorProvider { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentListRepository(): com.instructure.pandautils.features.assignments.list.AssignmentListRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideAssignmentListBehavior(): com.instructure.pandautils.features.assignments.list.AssignmentListBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideCalendarRepository(): com.instructure.pandautils.features.calendar.CalendarRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideCalendarBehavior(): com.instructure.pandautils.features.calendar.CalendarBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideCreateUpdateEventRepository(): com.instructure.pandautils.features.calendarevent.createupdate.CreateUpdateEventRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideCreateUpdateEventViewModelBehavior(): com.instructure.pandautils.features.calendarevent.createupdate.CreateUpdateEventViewModelBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideCreateUpdateToDoRepository(): com.instructure.pandautils.features.calendartodo.createupdate.CreateUpdateToDoRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideCreateUpdateToDoViewModelBehavior(): com.instructure.pandautils.features.calendartodo.createupdate.CreateUpdateToDoViewModelBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideEditDashboardRepository(): com.instructure.pandautils.features.dashboard.edit.EditDashboardRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideEventViewModelBehavior(): com.instructure.pandautils.features.calendarevent.details.EventViewModelBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideGradesBehaviour(): com.instructure.pandautils.features.grades.GradesBehaviour { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideGradesRepository(): com.instructure.pandautils.features.grades.GradesRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideHelpLinkFilter(): com.instructure.pandautils.features.help.HelpLinkFilter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideInboxComposeRepository(): com.instructure.pandautils.features.inbox.compose.InboxComposeRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideInboxComposeBehavior(): com.instructure.pandautils.features.inbox.compose.InboxComposeBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideInboxDetailsBehavior(): com.instructure.pandautils.features.inbox.details.InboxDetailsBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideInboxRepository(): com.instructure.pandautils.features.inbox.list.InboxRepository { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideSettingsBehaviour(): com.instructure.pandautils.features.settings.SettingsBehaviour { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideSpeedGraderPostPolicyRouter(): com.instructure.pandautils.features.speedgrader.SpeedGraderPostPolicyRouter { + throw NotImplementedError("This is a test module. Implementation not required.") + } + + @Provides + fun provideToDoViewModelBehavior(): com.instructure.pandautils.features.calendartodo.details.ToDoViewModelBehavior { + throw NotImplementedError("This is a test module. Implementation not required.") + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/dashboard/HorizonDashboardInteractionTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/dashboard/HorizonDashboardInteractionTest.kt new file mode 100644 index 0000000000..748b3ad162 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/dashboard/HorizonDashboardInteractionTest.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.interaction.features.dashboard + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addItemToModule +import com.instructure.canvas.espresso.mockcanvas.addModuleToCourse +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeGetHorizonCourseManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeGetProgramsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeGetSkillsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeGetWidgetsManager +import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvasapi2.di.graphql.GetCoursesModule +import com.instructure.canvasapi2.di.graphql.JourneyModule +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetProgramsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetSkillsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetWidgetsManager +import com.instructure.canvasapi2.models.Page +import com.instructure.horizon.espresso.HorizonTest +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules +import kotlinx.coroutines.runBlocking +import org.junit.Test + +@HiltAndroidTest +@UninstallModules(GetCoursesModule::class, JourneyModule::class) +class HorizonDashboardInteractionTest: HorizonTest() { + private val fakeGetHorizonCourseManager = FakeGetHorizonCourseManager() + private val fakeGetProgramsManager = FakeGetProgramsManager() + private val fakeGetWidgetsManager = FakeGetWidgetsManager() + private val fakeGetSkillsManager = FakeGetSkillsManager() + + @BindValue + @JvmField + val getProgramsManager: GetProgramsManager = fakeGetProgramsManager + + @BindValue + @JvmField + val getWidgetsManager: GetWidgetsManager = fakeGetWidgetsManager + + @BindValue + @JvmField + val getSkillsManager: GetSkillsManager = fakeGetSkillsManager + + @BindValue + @JvmField + val getCoursesManager: HorizonGetCoursesManager = fakeGetHorizonCourseManager + + @Test + fun testDashboardCards() { + val data = MockCanvas.init( + studentCount = 1, + teacherCount = 1, + courseCount = 3 + ) + val course1 = data.courses.values.toList()[0] + val course2 = data.courses.values.toList()[1] + val module1 = data.addModuleToCourse(course1, "Module 0") + val module2 = data.addModuleToCourse(course2, "Module 1") + val moduleItem1 = data.addItemToModule(course1, module1.id, Page(title = "Module Item 1")) + val moduleItem2 = data.addItemToModule(course2, module2.id, Page(title = "Module Item 2")) + val student = data.students.first() + val token = data.tokenFor(student)!! + tokenLogin(data.domain, token, student) + + val programs = runBlocking { fakeGetProgramsManager.getPrograms(false) } + dashboardPage.assertNotStartedProgramDisplayed(programs[1].name) + dashboardPage.assertCourseCardDisplayed( + course1.name, + listOf(programs[0].name), + fakeGetHorizonCourseManager.getCourses().first().progress, + moduleItem1.title + ) + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/home/HorizonHomeInteractionTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/home/HorizonHomeInteractionTest.kt new file mode 100644 index 0000000000..d7d3d6549f --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/home/HorizonHomeInteractionTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.interaction.features.home + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeGetHorizonCourseManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeGetProgramsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeGetSkillsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeGetWidgetsManager +import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvasapi2.di.graphql.GetCoursesModule +import com.instructure.canvasapi2.di.graphql.JourneyModule +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetProgramsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetSkillsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetWidgetsManager +import com.instructure.horizon.espresso.HorizonTest +import com.instructure.horizon.pages.HorizonHomePage +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules +import org.junit.Test + +@HiltAndroidTest +@UninstallModules(GetCoursesModule::class, JourneyModule::class) +class HorizonHomeInteractionTest : HorizonTest() { + private val fakeGetHorizonCourseManager = FakeGetHorizonCourseManager() + private val fakeGetProgramsManager = FakeGetProgramsManager() + private val fakeGetWidgetsManager = FakeGetWidgetsManager() + private val fakeGetSkillsManager = FakeGetSkillsManager() + + @BindValue + @JvmField + val getProgramsManager: GetProgramsManager = fakeGetProgramsManager + + @BindValue + @JvmField + val getWidgetsManager: GetWidgetsManager = fakeGetWidgetsManager + + @BindValue + @JvmField + val getSkillsManager: GetSkillsManager = fakeGetSkillsManager + + @BindValue + @JvmField + val getCoursesManager: HorizonGetCoursesManager = fakeGetHorizonCourseManager + + private val homePage: HorizonHomePage by lazy { HorizonHomePage(composeTestRule) } + + @Test + fun testBottomNavigationAfterLogin() { + val data = MockCanvas.init( + studentCount = 1, + teacherCount = 1, + courseCount = 1 + ) + val student = data.students.first() + val token = data.tokenFor(student)!! + tokenLogin(data.domain, token, student) + + homePage.assertBottomNavigationVisible() + } + + @Test + fun testNavigationBetweenTabs() { + val data = MockCanvas.init( + studentCount = 1, + teacherCount = 1, + courseCount = 1 + ) + val student = data.students.first() + val token = data.tokenFor(student)!! + tokenLogin(data.domain, token, student) + + homePage.assertBottomNavigationVisible() + homePage.clickLearnTab() + homePage.clickHomeTab() + homePage.clickHomeTab() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/notification/HorizonNotificationInteractionTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/notification/HorizonNotificationInteractionTest.kt new file mode 100644 index 0000000000..18a6af0362 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/interaction/features/notification/HorizonNotificationInteractionTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.interaction.features.notification + +import com.instructure.canvas.espresso.mockcanvas.MockCanvas +import com.instructure.canvas.espresso.mockcanvas.addAccountNotification +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeGetHorizonCourseManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeGetProgramsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeGetSkillsManager +import com.instructure.canvas.espresso.mockcanvas.fakes.FakeGetWidgetsManager +import com.instructure.canvas.espresso.mockcanvas.init +import com.instructure.canvasapi2.di.graphql.GetCoursesModule +import com.instructure.canvasapi2.di.graphql.JourneyModule +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetProgramsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetSkillsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetWidgetsManager +import com.instructure.horizon.espresso.HorizonTest +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules +import org.junit.Test + +@HiltAndroidTest +@UninstallModules(GetCoursesModule::class, JourneyModule::class) +class HorizonNotificationInteractionTest: HorizonTest() { + private val fakeGetHorizonCourseManager = FakeGetHorizonCourseManager() + private val fakeGetProgramsManager = FakeGetProgramsManager() + private val fakeGetWidgetsManager = FakeGetWidgetsManager() + private val fakeGetSkillsManager = FakeGetSkillsManager() + + @BindValue + @JvmField + val getProgramsManager: GetProgramsManager = fakeGetProgramsManager + + @BindValue + @JvmField + val getWidgetsManager: GetWidgetsManager = fakeGetWidgetsManager + + @BindValue + @JvmField + val getSkillsManager: GetSkillsManager = fakeGetSkillsManager + + @BindValue + @JvmField + val getCoursesManager: HorizonGetCoursesManager = fakeGetHorizonCourseManager + + @Test + fun testNotifications() { + val data = MockCanvas.init( + studentCount = 1, + teacherCount = 1, + courseCount = 3 + ) + val student = data.students.first() + val token = data.tokenFor(student)!! + tokenLogin(data.domain, token, student) + + val accountNotification = data.addAccountNotification() + dashboardPage.clickNotificationButton() + notificationsPage.assertNotificationItem(accountNotification.subject, "Announcement") + + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonDashboardPage.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonDashboardPage.kt new file mode 100644 index 0000000000..dec33fa92c --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonDashboardPage.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.pages + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasAnyDescendant +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onChildren +import androidx.compose.ui.test.onFirst +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onParent +import androidx.compose.ui.test.performClick +import kotlin.math.roundToInt + +class HorizonDashboardPage(private val composeTestRule: ComposeTestRule) { + fun assertNotStartedProgramDisplayed(programName: String) { + composeTestRule.onNodeWithText(programName, substring = true) + .assertIsDisplayed() + + composeTestRule.onNodeWithText("Program details", useUnmergedTree = true) + .assertIsDisplayed() + } + + fun clickProgramDetails(programName: String) { + composeTestRule.onNodeWithText("Program details", useUnmergedTree = true) + .assertIsDisplayed() + .performClick() + } + + fun assertCourseCardDisplayed( + courseName: String, + programNames: List = emptyList(), + progress: Double? = null, + moduleItemName: String? = null + ) { + val courseCardParent = composeTestRule.onNodeWithText(courseName) + .assertIsDisplayed() + .assertHasClickAction() + .onParent() + .onParent() + + courseCardParent.onChildren() + .filterToOne(hasAnyDescendant(hasText(courseName))) + .assertIsDisplayed() + + programNames.forEach { programName -> + courseCardParent.onChildren() + .filterToOne(hasAnyDescendant(hasText(programName, substring = true))) + .assertIsDisplayed() + } + + if (progress != null) { + courseCardParent.onChildren() + .filterToOne(hasAnyDescendant(hasText(progress.roundToInt().toString() + "%", substring = true))) + .assertIsDisplayed() + } + + if (moduleItemName != null) { + courseCardParent.onChildren() + .filterToOne(hasAnyDescendant(hasText(moduleItemName))) + .onChildren().onFirst() + .assertIsDisplayed() + .assertHasClickAction() + } + } + + fun clickCourseCard(courseName: String) { + composeTestRule.onNodeWithText(courseName) + .performClick() + } + + fun clickCourseCardModuleItem(courseName: String, moduleItemName: String) { + composeTestRule.onNodeWithText(courseName) + .assertIsDisplayed() + .assertHasClickAction() + .onParent() + .onParent() + .onChildren() + .filterToOne(hasAnyDescendant(hasText(moduleItemName))) + .onChildren().onFirst() + .assertIsDisplayed() + .performClick() + } + + fun clickInboxButton() { + composeTestRule.onNodeWithContentDescription("Inbox").performClick() + } + + fun clickNotificationButton() { + composeTestRule.onNodeWithContentDescription("Notifications").performClick() + } + + fun clickNotebookButton() { + composeTestRule.onNodeWithContentDescription("Notebook").performClick() + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonHomePage.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonHomePage.kt new file mode 100644 index 0000000000..bf6af8a6ca --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonHomePage.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.pages + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick + +class HorizonHomePage(private val composeTestRule: ComposeTestRule) { + fun assertBottomNavigationVisible() { + composeTestRule.onNodeWithText("Home") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Learn") + .assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("AI assist") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Skillspace") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Account") + .assertIsDisplayed() + } + + fun clickHomeTab() { + composeTestRule.onNodeWithText("Home") + .performClick() + } + + fun clickLearnTab() { + composeTestRule.onNodeWithText("Learn") + .performClick() + } + + fun clickAiAssistantTab() { + composeTestRule.onNodeWithContentDescription("AI assist") + .performClick() + } + + fun clickSkillspaceTab() { + composeTestRule.onNodeWithText("Skillspace") + .performClick() + } + + fun clickAccountTab() { + composeTestRule.onNodeWithText("Account") + .performClick() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonInboxPage.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonInboxPage.kt new file mode 100644 index 0000000000..7d15e29cad --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonInboxPage.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.pages + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick + +class HorizonInboxPage(private val composeTestRule: ComposeTestRule) { + fun assertInboxItemDisplayed(subject: String) { + composeTestRule.onNodeWithText(subject) + .assertIsDisplayed() + } + + fun clickInboxItem(subject: String) { + composeTestRule.onNodeWithText(subject) + .performClick() + } + + fun clickComposeButton() { + composeTestRule.onNodeWithContentDescription("Compose") + .performClick() + } + + fun assertEmptyState() { + composeTestRule.onNode(hasText("No messages", substring = true)) + .assertIsDisplayed() + } + + fun assertConversationCount(count: Int) { + composeTestRule.onNodeWithText("$count conversations", substring = true) + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonLearnPage.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonLearnPage.kt new file mode 100644 index 0000000000..2acbc847d2 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonLearnPage.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.pages + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick + +class HorizonLearnPage(private val composeTestRule: ComposeTestRule) { + fun assertCourseDisplayed(courseName: String) { + composeTestRule.onNodeWithText(courseName) + .assertIsDisplayed() + } + + fun clickCourse(courseName: String) { + composeTestRule.onNodeWithText(courseName) + .performClick() + } + + fun assertProgramDisplayed(programName: String) { + composeTestRule.onNodeWithText(programName) + .assertIsDisplayed() + } + + fun clickProgram(programName: String) { + composeTestRule.onNodeWithText(programName) + .performClick() + } + + fun assertEmptyState() { + composeTestRule.onNodeWithText("No courses", substring = true) + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonModuleItemSequencePage.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonModuleItemSequencePage.kt new file mode 100644 index 0000000000..fa5a3d076b --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonModuleItemSequencePage.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.pages + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick + +class HorizonModuleItemSequencePage(private val composeTestRule: ComposeTestRule) { + fun assertModuleItemDisplayed(title: String) { + composeTestRule.onNodeWithText(title) + .assertIsDisplayed() + } + + fun clickNextButton() { + composeTestRule.onNodeWithContentDescription("Next") + .performClick() + } + + fun clickPreviousButton() { + composeTestRule.onNodeWithContentDescription("Previous") + .performClick() + } + + fun clickProgressButton() { + composeTestRule.onNodeWithContentDescription("Progress") + .performClick() + } + + fun assertProgressDisplayed() { + composeTestRule.onNodeWithText("Progress", substring = true) + .assertIsDisplayed() + } + + fun clickMarkAsDone() { + composeTestRule.onNodeWithText("Mark as done") + .performClick() + } + + fun assertMarkedAsDone() { + composeTestRule.onNodeWithText("Marked as done", substring = true) + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonNotificationPage.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonNotificationPage.kt new file mode 100644 index 0000000000..772bf5259b --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonNotificationPage.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.pages + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasAnyChild +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onChild +import androidx.compose.ui.test.performClick + +class HorizonNotificationPage(private val composeTestRule: ComposeTestRule) { + fun assertNotificationItem(title: String, label: String) { + composeTestRule.onNode( + hasAnyChild(hasText(label)).and( + hasAnyChild(hasText(title)) + ) + ).assertIsDisplayed().onChild().assertHasClickAction() + } + + fun clickNotificationItem(title: String, label: String) { + composeTestRule.onNode( + hasAnyChild(hasText(label)).and( + hasAnyChild(hasText(title)) + ) + ).onChild().performClick() + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/HiltTestActivity.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/HiltTestActivity.kt new file mode 100644 index 0000000000..8e60d0c5ef --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/HiltTestActivity.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.ui + +import androidx.activity.ComponentActivity +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class HiltTestActivity : ComponentActivity() diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/course/HorizonDashboardCourseSectionUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/course/HorizonDashboardCourseSectionUiTest.kt new file mode 100644 index 0000000000..e48e6049f2 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/course/HorizonDashboardCourseSectionUiTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.ui.features.dashboard.course + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performScrollTo +import androidx.navigation.compose.rememberNavController +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardButtonRoute +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardItemState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardState +import com.instructure.horizon.features.dashboard.widget.course.DashboardCourseSection +import com.instructure.horizon.features.dashboard.widget.course.DashboardCourseUiState +import com.instructure.horizon.features.dashboard.widget.course.card.CardClickAction +import com.instructure.horizon.features.dashboard.widget.course.card.DashboardCourseCardModuleItemState +import com.instructure.horizon.features.dashboard.widget.course.card.DashboardCourseCardParentProgramState +import com.instructure.horizon.features.dashboard.widget.course.card.DashboardCourseCardState +import com.instructure.horizon.model.LearningObjectType +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Date + +@RunWith(AndroidJUnit4::class) +class HorizonDashboardCourseSectionUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testProgramAndCourseCards() { + val state = DashboardCourseUiState( + state = DashboardItemState.SUCCESS, + programs = DashboardPaginatedWidgetCardState( + listOf( + DashboardPaginatedWidgetCardItemState( + title = "Program 1", + route = DashboardPaginatedWidgetCardButtonRoute.HomeRoute("") + ) + ) + ), + courses = listOf( + DashboardCourseCardState( + parentPrograms = listOf( + DashboardCourseCardParentProgramState( + programName = "Program 11", + programId = "1", + onClickAction = CardClickAction.Action {} + ), + DashboardCourseCardParentProgramState( + programName = "Program 12", + programId = "2", + onClickAction = CardClickAction.Action {} + ) + ), + title = "Course 1", + moduleItem = DashboardCourseCardModuleItemState( + moduleItemTitle = "Module Item 1", + moduleItemType = LearningObjectType.PAGE, + dueDate = Date(), + estimatedDuration = "5 min", + onClickAction = CardClickAction.Action {} + ) + ), + DashboardCourseCardState( + title = "Course 2", + moduleItem = DashboardCourseCardModuleItemState( + moduleItemTitle = "Module Item 2", + moduleItemType = LearningObjectType.ASSIGNMENT, + dueDate = Date(), + estimatedDuration = "10 min", + onClickAction = CardClickAction.Action {} + ) + ) + ) + ) + composeTestRule.setContent { + val mainNavController = rememberNavController() + val homeNavController = rememberNavController() + DashboardCourseSection(state, mainNavController,homeNavController) + } + + composeTestRule.onNodeWithText("Program 1").assertExists() + + composeTestRule.onNodeWithText("Course 1").performScrollTo().assertExists() + composeTestRule.onNodeWithText("Program 11", true).assertExists() + composeTestRule.onNodeWithText("Program 12", true).assertExists() + composeTestRule.onNodeWithText("Module Item 1").assertExists().assertHasClickAction() + composeTestRule.onNodeWithText("5 min").assertExists().assertHasClickAction() + composeTestRule.onNodeWithText("Page").assertExists().assertHasClickAction() + + composeTestRule.onNodeWithText("Course 2").performScrollTo().assertExists() + composeTestRule.onNodeWithText("Module Item 2").assertExists().assertHasClickAction() + composeTestRule.onNodeWithText("10 min").assertExists().assertHasClickAction() + composeTestRule.onNodeWithText("Assignment").assertExists().assertHasClickAction() + } + +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/widget/announcement/DashboardAnnouncementBannerWidgetUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/widget/announcement/DashboardAnnouncementBannerWidgetUiTest.kt new file mode 100644 index 0000000000..d700edea7e --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/widget/announcement/DashboardAnnouncementBannerWidgetUiTest.kt @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.ui.features.dashboard.widget.announcement + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.navigation.compose.rememberNavController +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardButtonRoute +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardChipState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardItemState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardState +import com.instructure.horizon.features.dashboard.widget.announcement.DashboardAnnouncementBannerSection +import com.instructure.horizon.features.dashboard.widget.announcement.DashboardAnnouncementBannerUiState +import com.instructure.horizon.horizonui.molecules.StatusChipColor +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Date + +@RunWith(AndroidJUnit4::class) +class DashboardAnnouncementBannerWidgetUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + + @Test + fun testLoadingStateDisplaysShimmerEffect() { + val uiState = DashboardAnnouncementBannerUiState( + state = DashboardItemState.LOADING, + onRefresh = { it() } + ) + + composeTestRule.setContent { + DashboardAnnouncementBannerSection(uiState, rememberNavController(), rememberNavController()) + } + + composeTestRule.waitForIdle() + } + + @Test + fun testErrorStateDisplaysErrorMessageAndRetryButton() { + var refreshCalled = false + val uiState = DashboardAnnouncementBannerUiState( + state = DashboardItemState.ERROR, + onRefresh = { onComplete -> + refreshCalled = true + onComplete() + } + ) + + composeTestRule.setContent { + DashboardAnnouncementBannerSection(uiState, rememberNavController(), rememberNavController()) + } + + val errorMessage = context.getString(R.string.dashboardAnnouncementBannerErrorMessage) + + composeTestRule.onNodeWithText(errorMessage, substring = true).assertIsDisplayed() + composeTestRule.onNodeWithText("Refresh") + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + + assert(refreshCalled) { "Refresh callback should be called when retry button is clicked" } + } + + @Test + fun testSuccessStateWithSingleAnnouncementDisplaysCorrectly() { + val testAnnouncement = DashboardPaginatedWidgetCardItemState( + chipState = DashboardPaginatedWidgetCardChipState( + label = "Announcement", + color = StatusChipColor.Sky + ), + title = "Important Announcement", + source = "Course Name", + date = Date(), + route = DashboardPaginatedWidgetCardButtonRoute.MainRoute("") + ) + + val uiState = DashboardAnnouncementBannerUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardPaginatedWidgetCardState( + listOf(testAnnouncement) + ), + onRefresh = { it() } + ) + + composeTestRule.setContent { + DashboardAnnouncementBannerSection(uiState, rememberNavController(), rememberNavController()) + } + + val announcementLabel = context.getString(R.string.notificationsAnnouncementCategoryLabel) + val fromLabel = context.getString(R.string.dashboardAnnouncementBannerFrom, "Course Name") + + composeTestRule.onNodeWithText(announcementLabel).assertIsDisplayed() + composeTestRule.onNodeWithText(fromLabel).assertIsDisplayed() + composeTestRule.onNodeWithText("Important Announcement").assertIsDisplayed() + } + + @Test + fun testSuccessStateWithMultipleAnnouncementsDisplaysAllItems() { + val announcements = listOf( + DashboardPaginatedWidgetCardItemState( + pageState = "1 of 2", + chipState = DashboardPaginatedWidgetCardChipState( + label = "Announcement", + color = StatusChipColor.Sky + ), + title = "First Announcement", + source = "Course 1", + date = Date(), + route = DashboardPaginatedWidgetCardButtonRoute.MainRoute("") + ), + DashboardPaginatedWidgetCardItemState( + pageState = "2 of 2", + chipState = DashboardPaginatedWidgetCardChipState( + label = "Announcement", + color = StatusChipColor.Sky + ), + title = "Second Announcement", + source = "Course 2", + date = Date(), + route = DashboardPaginatedWidgetCardButtonRoute.MainRoute("") + ) + ) + + val uiState = DashboardAnnouncementBannerUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardPaginatedWidgetCardState(announcements), + onRefresh = { it() } + ) + + composeTestRule.setContent { + DashboardAnnouncementBannerSection(uiState, rememberNavController(), rememberNavController()) + } + + composeTestRule.onNodeWithText("First Announcement").assertIsDisplayed() + composeTestRule.onNodeWithText( + context.getString(R.string.dashboardAnnouncementBannerFrom, "Course 1") + ).assertIsDisplayed() + + composeTestRule.onNodeWithText("1 of 2", useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun testSuccessStateWithGlobalAnnouncementWithoutSource() { + val testAnnouncement = DashboardPaginatedWidgetCardItemState( + chipState = DashboardPaginatedWidgetCardChipState( + label = "Announcement", + color = StatusChipColor.Sky + ), + title = "Global Announcement", + source = null, + date = Date(), + route = DashboardPaginatedWidgetCardButtonRoute.MainRoute("") + ) + + val uiState = DashboardAnnouncementBannerUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardPaginatedWidgetCardState(listOf(testAnnouncement)), + onRefresh = { it() } + ) + + composeTestRule.setContent { + DashboardAnnouncementBannerSection(uiState, rememberNavController(), rememberNavController()) + } + + val announcementLabel = context.getString(R.string.notificationsAnnouncementCategoryLabel) + + composeTestRule.onNodeWithText(announcementLabel).assertIsDisplayed() + composeTestRule.onNodeWithText("Global Announcement").assertIsDisplayed() + } + + @Test + fun testSuccessStateAnnouncementIsClickable() { + val testAnnouncement = DashboardPaginatedWidgetCardItemState( + chipState = DashboardPaginatedWidgetCardChipState( + label = "Announcement", + color = StatusChipColor.Sky + ), + title = "Test Announcement", + source = "Test Course", + date = Date(), + route = DashboardPaginatedWidgetCardButtonRoute.MainRoute("") + ) + + val uiState = DashboardAnnouncementBannerUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardPaginatedWidgetCardState(listOf(testAnnouncement)), + onRefresh = { it() } + ) + + composeTestRule.setContent { + DashboardAnnouncementBannerSection(uiState, rememberNavController(), rememberNavController()) + } + + + composeTestRule.onNodeWithText(testAnnouncement.title!!) + .assertIsDisplayed() + } + + @Test + fun testSuccessStateDisplaysDateCorrectly() { + val testDate = Date(1704067200000L) + val testAnnouncement = DashboardPaginatedWidgetCardItemState( + chipState = DashboardPaginatedWidgetCardChipState( + label = "Announcement", + color = StatusChipColor.Sky + ), + title = "Dated Announcement", + source = "Test Course", + date = testDate, + route = DashboardPaginatedWidgetCardButtonRoute.MainRoute("") + ) + + val uiState = DashboardAnnouncementBannerUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardPaginatedWidgetCardState(listOf(testAnnouncement)), + onRefresh = { it() } + ) + + composeTestRule.setContent { + DashboardAnnouncementBannerSection(uiState, rememberNavController(), rememberNavController()) + } + + composeTestRule.onNodeWithText("Jan 01, 2024").assertIsDisplayed() + } + + @Test + fun testSuccessStateWithAnnouncementWithoutDate() { + val testAnnouncement = DashboardPaginatedWidgetCardItemState( + chipState = DashboardPaginatedWidgetCardChipState( + label = "Announcement", + color = StatusChipColor.Sky + ), + title = "No Date Announcement", + source = "Test Course", + date = null, + route = DashboardPaginatedWidgetCardButtonRoute.MainRoute("") + ) + + val uiState = DashboardAnnouncementBannerUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardPaginatedWidgetCardState(listOf(testAnnouncement)), + onRefresh = { it() } + ) + + composeTestRule.setContent { + DashboardAnnouncementBannerSection(uiState, rememberNavController(), rememberNavController()) + } + + composeTestRule.onNodeWithText("No Date Announcement").assertIsDisplayed() + } + + @Test + fun testSuccessStateWithLongAnnouncementTitle() { + val longTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. This is a very long announcement title that should be displayed properly in the widget." + val testAnnouncement = DashboardPaginatedWidgetCardItemState( + chipState = DashboardPaginatedWidgetCardChipState( + label = "Announcement", + color = StatusChipColor.Sky + ), + title = longTitle, + source = "Test Course", + date = Date(), + route = DashboardPaginatedWidgetCardButtonRoute.MainRoute("") + ) + + val uiState = DashboardAnnouncementBannerUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardPaginatedWidgetCardState(listOf(testAnnouncement)), + onRefresh = { it() } + ) + + composeTestRule.setContent { + DashboardAnnouncementBannerSection(uiState, rememberNavController(), rememberNavController()) + } + + composeTestRule.onNodeWithText(longTitle).assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/widget/myprogress/DashboardMyProgressWidgetUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/widget/myprogress/DashboardMyProgressWidgetUiTest.kt new file mode 100644 index 0000000000..db34581605 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/widget/myprogress/DashboardMyProgressWidgetUiTest.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.ui.features.dashboard.widget.myprogress + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.myprogress.DashboardMyProgressSection +import com.instructure.horizon.features.dashboard.widget.myprogress.DashboardMyProgressUiState +import com.instructure.horizon.features.dashboard.widget.myprogress.card.DashboardMyProgressCardState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DashboardMyProgressWidgetUiTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testLoadingStateDisplaysShimmerEffect() { + val state = DashboardMyProgressUiState( + state = DashboardItemState.LOADING + ) + + composeTestRule.setContent { + DashboardMyProgressSection(state) + } + + composeTestRule.waitForIdle() + } + + @Test + fun testErrorStateDisplaysErrorMessageAndRetryButton() { + var refreshCalled = false + val state = DashboardMyProgressUiState( + state = DashboardItemState.ERROR, + onRefresh = { onComplete -> + refreshCalled = true + onComplete() + } + ) + + composeTestRule.setContent { + DashboardMyProgressSection(state) + } + + composeTestRule.onNodeWithText("Activities").assertIsDisplayed() + + composeTestRule.onNodeWithText("We weren't able to load this content.\nPlease try again.", substring = true) + .assertIsDisplayed() + + composeTestRule.onNodeWithText("Refresh", useUnmergedTree = true) + .assertIsDisplayed() + .performClick() + + assert(refreshCalled) { "Refresh callback should be called when retry button is clicked" } + } + + @Test + fun testSuccessStateDisplaysModuleCountAndTitle() { + val state = DashboardMyProgressUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardMyProgressCardState( + moduleCountCompleted = 5 + ) + ) + + composeTestRule.setContent { + DashboardMyProgressSection(state) + } + + composeTestRule.onNodeWithText("Activities").assertIsDisplayed() + + composeTestRule.onNodeWithText("5", useUnmergedTree = true).assertIsDisplayed() + + composeTestRule.onNodeWithText("completed", useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun testEmptyState() { + val state = DashboardMyProgressUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardMyProgressCardState( + moduleCountCompleted = 0 + ) + ) + + composeTestRule.setContent { + DashboardMyProgressSection(state) + } + + composeTestRule.onNodeWithText("This widget will update once data becomes available.") + .assertIsDisplayed() + + composeTestRule.onNodeWithText("0").assertDoesNotExist() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsWidgetUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsWidgetUiTest.kt new file mode 100644 index 0000000000..68630a8ccc --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsWidgetUiTest.kt @@ -0,0 +1,180 @@ +package com.instructure.horizon.ui.features.dashboard.widget.skillhighlights + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.navigation.compose.rememberNavController +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.skillhighlights.DashboardSkillHighlightsSection +import com.instructure.horizon.features.dashboard.widget.skillhighlights.DashboardSkillHighlightsUiState +import com.instructure.horizon.features.dashboard.widget.skillhighlights.card.DashboardSkillHighlightsCardState +import com.instructure.horizon.features.dashboard.widget.skillhighlights.card.SkillHighlight +import com.instructure.horizon.features.dashboard.widget.skillhighlights.card.SkillHighlightProficiencyLevel +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DashboardSkillHighlightsWidgetUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + + @Test + fun testLoadingStateDisplaysCorrectly() { + val uiState = DashboardSkillHighlightsUiState( + state = DashboardItemState.LOADING + ) + + composeTestRule.setContent { + DashboardSkillHighlightsSection(uiState, rememberNavController()) + } + + val title = context.getString(R.string.dashboardSkillHighlightsTitle) + composeTestRule.onNodeWithText(title, useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun testErrorStateDisplaysCorrectly() { + val uiState = DashboardSkillHighlightsUiState( + state = DashboardItemState.ERROR, + onRefresh = { it() } + ) + + composeTestRule.setContent { + DashboardSkillHighlightsSection(uiState, rememberNavController()) + } + + val title = context.getString(R.string.dashboardSkillHighlightsTitle) + val errorMessage = context.getString(R.string.dashboardWidgetCardErrorMessage) + val retryLabel = context.getString(R.string.dashboardSkillHighlightsRetry) + + composeTestRule.onNodeWithText(title).assertIsDisplayed() + composeTestRule.onNodeWithText(errorMessage, useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNodeWithText(retryLabel, useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun testErrorStateRefreshButtonWorks() { + var refreshCalled = false + val uiState = DashboardSkillHighlightsUiState( + state = DashboardItemState.ERROR, + onRefresh = { refreshCalled = true; it() } + ) + + composeTestRule.setContent { + DashboardSkillHighlightsSection(uiState, rememberNavController()) + } + + val retryLabel = context.getString(R.string.dashboardSkillHighlightsRetry) + composeTestRule.onNodeWithText(retryLabel, useUnmergedTree = true).performClick() + + assert(refreshCalled) + } + + @Test + fun testNoDataStateDisplaysCorrectly() { + val uiState = DashboardSkillHighlightsUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardSkillHighlightsCardState(skills = emptyList()) + ) + + composeTestRule.setContent { + DashboardSkillHighlightsSection(uiState, rememberNavController()) + } + + val title = context.getString(R.string.dashboardSkillHighlightsTitle) + val noDataTitle = context.getString(R.string.dashboardSkillHighlightsNoDataTitle) + val noDataMessage = context.getString(R.string.dashboardSkillHighlightsNoDataMessage) + + composeTestRule.onNodeWithText(title).assertIsDisplayed() + composeTestRule.onNodeWithText(noDataTitle).assertIsDisplayed() + composeTestRule.onNodeWithText(noDataMessage).assertIsDisplayed() + } + + @Test + fun testSuccessStateWithSkillsDisplaysCorrectly() { + val skills = listOf( + SkillHighlight("Advanced JavaScript", SkillHighlightProficiencyLevel.ADVANCED), + SkillHighlight("Python Programming", SkillHighlightProficiencyLevel.PROFICIENT), + SkillHighlight("Data Analysis", SkillHighlightProficiencyLevel.BEGINNER) + ) + val uiState = DashboardSkillHighlightsUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardSkillHighlightsCardState(skills = skills) + ) + + composeTestRule.setContent { + DashboardSkillHighlightsSection(uiState, rememberNavController()) + } + + val title = context.getString(R.string.dashboardSkillHighlightsTitle) + val advancedLabel = context.getString(R.string.dashboardSkillProficienyLevelAdvanced) + val proficientLabel = context.getString(R.string.dashboardSkillProficienyLevelProficient) + val beginnerLabel = context.getString(R.string.dashboardSkillProficienyLevelBeginner) + + composeTestRule.onNodeWithText(title).assertIsDisplayed() + composeTestRule.onNodeWithText("Advanced JavaScript").assertIsDisplayed() + composeTestRule.onNodeWithText("Python Programming").assertIsDisplayed() + composeTestRule.onNodeWithText("Data Analysis").assertIsDisplayed() + composeTestRule.onNodeWithText(advancedLabel).assertIsDisplayed() + composeTestRule.onNodeWithText(proficientLabel).assertIsDisplayed() + composeTestRule.onNodeWithText(beginnerLabel).assertIsDisplayed() + } + + @Test + fun testAllProficiencyLevelsDisplay() { + val skills = listOf( + SkillHighlight("Expert Skill", SkillHighlightProficiencyLevel.EXPERT), + SkillHighlight("Advanced Skill", SkillHighlightProficiencyLevel.ADVANCED), + SkillHighlight("Proficient Skill", SkillHighlightProficiencyLevel.PROFICIENT) + ) + val uiState = DashboardSkillHighlightsUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardSkillHighlightsCardState(skills = skills) + ) + + composeTestRule.setContent { + DashboardSkillHighlightsSection(uiState, rememberNavController()) + } + + val expertLabel = context.getString(R.string.dashboardSkillProficienyLevelExpert) + val advancedLabel = context.getString(R.string.dashboardSkillProficienyLevelAdvanced) + val proficientLabel = context.getString(R.string.dashboardSkillProficienyLevelProficient) + + composeTestRule.onNodeWithText(expertLabel).assertIsDisplayed() + composeTestRule.onNodeWithText(advancedLabel).assertIsDisplayed() + composeTestRule.onNodeWithText(proficientLabel).assertIsDisplayed() + } + + @Test + fun testLongSkillNameIsDisplayed() { + val skills = listOf( + SkillHighlight( + "This is a very long skill name that should be displayed correctly in the UI", + SkillHighlightProficiencyLevel.ADVANCED + ), + SkillHighlight("Short Skill", SkillHighlightProficiencyLevel.PROFICIENT), + SkillHighlight("Medium Length Skill Name", SkillHighlightProficiencyLevel.BEGINNER) + ) + val uiState = DashboardSkillHighlightsUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardSkillHighlightsCardState(skills = skills) + ) + + composeTestRule.setContent { + DashboardSkillHighlightsSection(uiState, rememberNavController()) + } + + // Text will be truncated but should still be findable by partial match + composeTestRule.onNodeWithText( + "This is a very long skill name that should be displayed correctly in the UI", + substring = true + ).assertIsDisplayed() + } +} \ No newline at end of file diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/widget/skilloverview/DashboardSkillOverviewWidgetUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/widget/skilloverview/DashboardSkillOverviewWidgetUiTest.kt new file mode 100644 index 0000000000..dbc99d6a41 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/widget/skilloverview/DashboardSkillOverviewWidgetUiTest.kt @@ -0,0 +1,171 @@ +package com.instructure.horizon.ui.features.dashboard.widget.skilloverview + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.navigation.compose.rememberNavController +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.skilloverview.DashboardSkillOverviewSection +import com.instructure.horizon.features.dashboard.widget.skilloverview.DashboardSkillOverviewUiState +import com.instructure.horizon.features.dashboard.widget.skilloverview.card.DashboardSkillOverviewCardState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DashboardSkillOverviewWidgetUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + + @Test + fun testLoadingStateDisplaysCorrectly() { + val uiState = DashboardSkillOverviewUiState( + state = DashboardItemState.LOADING + ) + + composeTestRule.setContent { + DashboardSkillOverviewSection(uiState, rememberNavController()) + } + + val title = context.getString(R.string.dashboardSkillOverviewTitle) + composeTestRule.onNodeWithText(title, useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun testErrorStateDisplaysCorrectly() { + val uiState = DashboardSkillOverviewUiState( + state = DashboardItemState.ERROR, + onRefresh = { it() } + ) + + composeTestRule.setContent { + DashboardSkillOverviewSection(uiState, rememberNavController()) + } + + val title = context.getString(R.string.dashboardSkillOverviewTitle) + val errorMessage = context.getString(R.string.dashboardWidgetCardErrorMessage) + val retryLabel = context.getString(R.string.dashboardWidgetCardErrorRetry) + + composeTestRule.onNodeWithText(title).assertIsDisplayed() + composeTestRule.onNodeWithText(errorMessage, substring = true).assertIsDisplayed() + composeTestRule.onNodeWithText(retryLabel, useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun testErrorStateRefreshButtonWorks() { + var refreshCalled = false + val uiState = DashboardSkillOverviewUiState( + state = DashboardItemState.ERROR, + onRefresh = { refreshCalled = true; it() } + ) + + composeTestRule.setContent { + DashboardSkillOverviewSection(uiState, rememberNavController()) + } + + val retryLabel = context.getString(R.string.dashboardWidgetCardErrorRetry) + composeTestRule.onNodeWithText(retryLabel, useUnmergedTree = true).performClick() + + assert(refreshCalled) + } + + @Test + fun testNoDataStateDisplaysCorrectly() { + val uiState = DashboardSkillOverviewUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardSkillOverviewCardState(completedSkillCount = 0) + ) + + composeTestRule.setContent { + DashboardSkillOverviewSection(uiState, rememberNavController()) + } + + val title = context.getString(R.string.dashboardSkillOverviewTitle) + val noDataMessage = context.getString(R.string.dashboardSkillOverviewNoDataMessage) + + composeTestRule.onNodeWithText(title).assertIsDisplayed() + composeTestRule.onNodeWithText(noDataMessage).assertIsDisplayed() + } + + @Test + fun testSuccessStateWithSkillCountDisplaysCorrectly() { + val uiState = DashboardSkillOverviewUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardSkillOverviewCardState(completedSkillCount = 5) + ) + + composeTestRule.setContent { + DashboardSkillOverviewSection(uiState, rememberNavController()) + } + + val title = context.getString(R.string.dashboardSkillOverviewTitle) + val earnedLabel = context.getString(R.string.dashboardSkillOverviewEarnedLabel) + + composeTestRule.onNodeWithText(title).assertIsDisplayed() + composeTestRule.onNodeWithText("5", useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNodeWithText(earnedLabel, useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun testSuccessStateWithSingleSkill() { + val uiState = DashboardSkillOverviewUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardSkillOverviewCardState(completedSkillCount = 1) + ) + + composeTestRule.setContent { + DashboardSkillOverviewSection(uiState, rememberNavController()) + } + + val title = context.getString(R.string.dashboardSkillOverviewTitle) + val earnedLabel = context.getString(R.string.dashboardSkillOverviewEarnedLabel) + + composeTestRule.onNodeWithText(title).assertIsDisplayed() + composeTestRule.onNodeWithText("1", useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNodeWithText(earnedLabel, useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun testSuccessStateWithLargeSkillCount() { + val uiState = DashboardSkillOverviewUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardSkillOverviewCardState(completedSkillCount = 99) + ) + + composeTestRule.setContent { + DashboardSkillOverviewSection(uiState, rememberNavController()) + } + + val title = context.getString(R.string.dashboardSkillOverviewTitle) + val earnedLabel = context.getString(R.string.dashboardSkillOverviewEarnedLabel) + + composeTestRule.onNodeWithText(title).assertIsDisplayed() + composeTestRule.onNodeWithText("99", useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNodeWithText(earnedLabel, useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun testSuccessStateWithVeryLargeSkillCount() { + val uiState = DashboardSkillOverviewUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardSkillOverviewCardState(completedSkillCount = 999) + ) + + composeTestRule.setContent { + DashboardSkillOverviewSection(uiState, rememberNavController()) + } + + val title = context.getString(R.string.dashboardSkillOverviewTitle) + val earnedLabel = context.getString(R.string.dashboardSkillOverviewEarnedLabel) + + composeTestRule.onNodeWithText(title).assertIsDisplayed() + composeTestRule.onNodeWithText("999", useUnmergedTree = true).assertIsDisplayed() + composeTestRule.onNodeWithText(earnedLabel, useUnmergedTree = true).assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/widget/timespent/DashboardTimeSpentWidgetUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/widget/timespent/DashboardTimeSpentWidgetUiTest.kt new file mode 100644 index 0000000000..a1929c52d4 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/dashboard/widget/timespent/DashboardTimeSpentWidgetUiTest.kt @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.ui.features.dashboard.widget.timespent + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.timespent.DashboardTimeSpentSection +import com.instructure.horizon.features.dashboard.widget.timespent.DashboardTimeSpentUiState +import com.instructure.horizon.features.dashboard.widget.timespent.card.CourseOption +import com.instructure.horizon.features.dashboard.widget.timespent.card.DashboardTimeSpentCardState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DashboardTimeSpentWidgetUiTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testLoadingStateDisplaysShimmerEffect() { + val state = DashboardTimeSpentUiState( + state = DashboardItemState.LOADING + ) + + composeTestRule.setContent { + DashboardTimeSpentSection(state) + } + + composeTestRule.waitForIdle() + } + + @Test + fun testErrorStateDisplaysErrorMessageAndRetryButton() { + var refreshCalled = false + val state = DashboardTimeSpentUiState( + state = DashboardItemState.ERROR, + onRefresh = { onComplete -> + refreshCalled = true + onComplete() + } + ) + + composeTestRule.setContent { + DashboardTimeSpentSection(state) + } + + composeTestRule.onNodeWithText("Time learning").assertIsDisplayed() + + composeTestRule.onNodeWithText("We weren't able to load this content.\nPlease try again.", substring = true) + .assertIsDisplayed() + + composeTestRule.onNodeWithText("Refresh", useUnmergedTree = true) + .assertIsDisplayed() + .performClick() + + assert(refreshCalled) { "Refresh callback should be called when retry button is clicked" } + } + + @Test + fun testSuccessStateDisplaysHoursAndTitleWithMultipleCourse() { + val state = DashboardTimeSpentUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardTimeSpentCardState( + hours = 12, + courses = listOf( + CourseOption(id = 1L, name = "Math 101"), + CourseOption(id = 2L, name = "Science 201") + ) + ) + ) + + composeTestRule.setContent { + DashboardTimeSpentSection(state) + } + + composeTestRule.onNodeWithText("Time learning").assertIsDisplayed() + + composeTestRule.onNodeWithText("12", useUnmergedTree = true).assertIsDisplayed() + + composeTestRule.onNodeWithText("hours", useUnmergedTree = true).assertIsDisplayed() + + composeTestRule.onNodeWithText("total", useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun testSuccessStateDisplaysMinutesAndTitleWithMultipleCourse() { + val state = DashboardTimeSpentUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardTimeSpentCardState( + minutes = 12, + courses = listOf( + CourseOption(id = 1L, name = "Math 101"), + CourseOption(id = 2L, name = "Science 201") + ) + ) + ) + + composeTestRule.setContent { + DashboardTimeSpentSection(state) + } + + composeTestRule.onNodeWithText("Time learning").assertIsDisplayed() + + composeTestRule.onNodeWithText("12", useUnmergedTree = true).assertIsDisplayed() + + composeTestRule.onNodeWithText("minutes", useUnmergedTree = true).assertIsDisplayed() + + composeTestRule.onNodeWithText("total", useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun testSuccessStateDisplaysHoursAndMinutesAndTitleWithMultipleCourse() { + val state = DashboardTimeSpentUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardTimeSpentCardState( + hours = 8, + minutes = 12, + courses = listOf( + CourseOption(id = 1L, name = "Math 101"), + CourseOption(id = 2L, name = "Science 201") + ) + ) + ) + + composeTestRule.setContent { + DashboardTimeSpentSection(state) + } + + composeTestRule.onNodeWithText("Time learning").assertIsDisplayed() + + composeTestRule.onNodeWithText("8", useUnmergedTree = true).assertIsDisplayed() + + composeTestRule.onNodeWithText("12", useUnmergedTree = true).assertIsDisplayed() + + composeTestRule.onNodeWithText("hrs", useUnmergedTree = true).assertIsDisplayed() + + composeTestRule.onNodeWithText("mins", useUnmergedTree = true).assertIsDisplayed() + + composeTestRule.onNodeWithText("total", useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun testSuccessStateDisplaysHoursAndTitleWithSingleCourse() { + val state = DashboardTimeSpentUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardTimeSpentCardState( + hours = 12, + courses = listOf( + CourseOption(id = 1L, name = "Math 101"), + ) + ) + ) + + composeTestRule.setContent { + DashboardTimeSpentSection(state) + } + + composeTestRule.onNodeWithText("Time learning").assertIsDisplayed() + + composeTestRule.onNodeWithText("12", useUnmergedTree = true).assertIsDisplayed() + + composeTestRule.onNodeWithText("hours", useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun testSuccessStateDisplaysMinutesAndTitleWithSingleCourse() { + val state = DashboardTimeSpentUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardTimeSpentCardState( + minutes = 12, + courses = listOf( + CourseOption(id = 1L, name = "Math 101"), + ) + ) + ) + + composeTestRule.setContent { + DashboardTimeSpentSection(state) + } + + composeTestRule.onNodeWithText("Time learning").assertIsDisplayed() + + composeTestRule.onNodeWithText("12", useUnmergedTree = true).assertIsDisplayed() + + composeTestRule.onNodeWithText("minutes", useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun testSuccessStateDisplaysHoursAndMinutesAndTitleWithSingleCourse() { + val state = DashboardTimeSpentUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardTimeSpentCardState( + hours = 8, + minutes = 12, + courses = listOf( + CourseOption(id = 1L, name = "Math 101"), + ) + ) + ) + + composeTestRule.setContent { + DashboardTimeSpentSection(state) + } + + composeTestRule.onNodeWithText("Time learning").assertIsDisplayed() + + composeTestRule.onNodeWithText("8", useUnmergedTree = true).assertIsDisplayed() + + composeTestRule.onNodeWithText("12", useUnmergedTree = true).assertIsDisplayed() + + composeTestRule.onNodeWithText("hrs", useUnmergedTree = true).assertIsDisplayed() + + composeTestRule.onNodeWithText("mins", useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun testSuccessStateWithZeroHoursZeroMinutesDisplaysEmpty() { + val state = DashboardTimeSpentUiState( + state = DashboardItemState.SUCCESS, + cardState = DashboardTimeSpentCardState( + hours = 0, + minutes = 0, + courses = listOf( + CourseOption(id = 1L, name = "Course 1") + ) + ) + ) + + composeTestRule.setContent { + DashboardTimeSpentSection(state) + } + + // Verify zero hours is not displayed + composeTestRule.onNodeWithText("0").assertDoesNotExist() + + // Verify single course text is not displayed + composeTestRule.onNodeWithText("hours in your course").assertDoesNotExist() + + // Verify empty state message is displayed + composeTestRule.onNodeWithText("This widget will update once data becomes available.").assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/home/HorizonHomeUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/home/HorizonHomeUiTest.kt new file mode 100644 index 0000000000..38b5a35024 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/home/HorizonHomeUiTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.ui.features.home + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.horizon.horizonui.molecules.Spinner +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@HiltAndroidTest +@RunWith(AndroidJUnit4::class) +class HorizonHomeUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testLoadingStateDisplaysSpinner() { + composeTestRule.setContent { + Spinner(modifier = Modifier.fillMaxSize()) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxComposeUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxComposeUiTest.kt new file mode 100644 index 0000000000..8f0a0950d9 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxComposeUiTest.kt @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.ui.features.inbox + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.text.input.TextFieldValue +import androidx.navigation.compose.rememberNavController +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Recipient +import com.instructure.horizon.features.inbox.attachment.HorizonInboxAttachmentPickerUiState +import com.instructure.horizon.features.inbox.compose.HorizonInboxComposeScreen +import com.instructure.horizon.features.inbox.compose.HorizonInboxComposeUiState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class HorizonInboxComposeUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + private val pickerState = HorizonInboxAttachmentPickerUiState() + + @Test + fun testComposeScreenDisplaysCourseSelector() { + val uiState = HorizonInboxComposeUiState( + coursePickerOptions = listOf( + Course(id = 1L, name = "Math 101"), + Course(id = 2L, name = "Science 202") + ) + ) + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithContentDescription("Select Course") + .assertIsDisplayed() + } + + @Test + fun testComposeScreenDisplaysRecipientField() { + val uiState = HorizonInboxComposeUiState() + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Recipient(s)") + .assertIsDisplayed() + } + + @Test + fun testComposeScreenDisplaysSubjectField() { + val uiState = HorizonInboxComposeUiState() + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Title/Subject") + .assertIsDisplayed() + } + + @Test + fun testComposeScreenDisplaysMessageField() { + val uiState = HorizonInboxComposeUiState() + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Message") + .assertIsDisplayed() + } + + @Test + fun testSendButtonDisabledWhenFieldsEmpty() { + val uiState = HorizonInboxComposeUiState() + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Send") + .assertIsDisplayed() + .assertHasClickAction() + } + + @Test + fun testSelectedCourseDisplays() { + val uiState = HorizonInboxComposeUiState( + selectedCourse = Course(id = 1L, name = "Math 101") + ) + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Math 101", useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun testSelectedRecipientsDisplay() { + val uiState = HorizonInboxComposeUiState( + selectedRecipients = listOf( + Recipient(stringId = "1", name = "John Doe"), + Recipient(stringId = "2", name = "Jane Smith") + ) + ) + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("John Doe") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Jane Smith") + .assertIsDisplayed() + } + + @Test + fun testSubjectTextDisplays() { + val uiState = HorizonInboxComposeUiState( + subject = TextFieldValue("Test Subject") + ) + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Test Subject") + .assertIsDisplayed() + } + + @Test + fun testBodyTextDisplays() { + val uiState = HorizonInboxComposeUiState( + body = TextFieldValue("Test message body") + ) + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Test message body") + .assertIsDisplayed() + } + + @Test + fun testSendIndividuallyCheckboxDisplays() { + val uiState = HorizonInboxComposeUiState() + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Send an individual message to each recipient") + .assertIsDisplayed() + } + + @Test + fun testErrorMessagesDisplay() { + val uiState = HorizonInboxComposeUiState( + courseErrorMessage = "Please select a course", + recipientErrorMessage = "Please select at least one recipient", + subjectErrorMessage = "Subject is required", + bodyErrorMessage = "Message is required" + ) + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Please select a course", useUnmergedTree = true) + .assertIsDisplayed() + composeTestRule.onNodeWithText("Please select at least one recipient") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Subject is required") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Message is required") + .assertIsDisplayed() + } + + @Test + fun testAttachmentButtonDisplays() { + val uiState = HorizonInboxComposeUiState() + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Attach file") + .assertIsDisplayed() + .assertHasClickAction() + } + + @Test + fun testLoadingStateDisplaysProgressIndicator() { + val uiState = HorizonInboxComposeUiState( + isSendLoading = true + ) + + composeTestRule.setContent { + HorizonInboxComposeScreen(uiState, pickerState, rememberNavController()) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxDetailsUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxDetailsUiTest.kt new file mode 100644 index 0000000000..2f6815a6a1 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxDetailsUiTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.ui.features.inbox + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.navigation.compose.rememberNavController +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.horizon.features.inbox.details.HorizonInboxDetailsItem +import com.instructure.horizon.features.inbox.details.HorizonInboxDetailsScreen +import com.instructure.horizon.features.inbox.details.HorizonInboxDetailsUiState +import com.instructure.horizon.horizonui.platform.LoadingState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Date + +@RunWith(AndroidJUnit4::class) +class HorizonInboxDetailsUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testDetailsScreenDisplaysTitle() { + val uiState = HorizonInboxDetailsUiState( + title = "Test Conversation", + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + HorizonInboxDetailsScreen(uiState, rememberNavController()) + } + + composeTestRule.onNodeWithText("Test Conversation") + .assertIsDisplayed() + } + + @Test + fun testDetailsScreenDisplaysMessages() { + val uiState = HorizonInboxDetailsUiState( + title = "Conversation", + items = listOf( + HorizonInboxDetailsItem( + author = "John Doe", + date = Date(), + isHtmlContent = false, + content = "Test message content", + attachments = emptyList() + ) + ), + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + HorizonInboxDetailsScreen(uiState, rememberNavController()) + } + + composeTestRule.onNodeWithText("John Doe") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Test message content") + .assertIsDisplayed() + } + + @Test + fun testLoadingStateDisplaysSpinner() { + val uiState = HorizonInboxDetailsUiState( + title = "Loading...", + loadingState = LoadingState(isLoading = true) + ) + + composeTestRule.setContent { + HorizonInboxDetailsScreen(uiState, rememberNavController()) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } + + @Test + fun testMultipleMessagesDisplay() { + val uiState = HorizonInboxDetailsUiState( + title = "Conversation", + items = listOf( + HorizonInboxDetailsItem( + author = "Student 1", + date = Date(), + isHtmlContent = false, + content = "First message", + attachments = emptyList() + ), + HorizonInboxDetailsItem( + author = "Teacher", + date = Date(), + isHtmlContent = false, + content = "Second message", + attachments = emptyList() + ), + HorizonInboxDetailsItem( + author = "Student 1", + date = Date(), + isHtmlContent = false, + content = "Third message", + attachments = emptyList() + ) + ), + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + HorizonInboxDetailsScreen(uiState, rememberNavController()) + } + + composeTestRule.onNodeWithText("First message") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Second message") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Third message") + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxListUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxListUiTest.kt new file mode 100644 index 0000000000..bf046a12f4 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/inbox/HorizonInboxListUiTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.ui.features.inbox + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.horizon.horizonui.molecules.Spinner +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class HorizonInboxListUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testLoadingStateDisplaysSpinner() { + composeTestRule.setContent { + Spinner(modifier = Modifier.fillMaxSize()) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } + + @Test + fun testInboxTitleDisplays() { + composeTestRule.setContent { + androidx.compose.material3.Text("Inbox") + } + + composeTestRule.onNodeWithText("Inbox") + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/learn/HorizonLearnUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/learn/HorizonLearnUiTest.kt new file mode 100644 index 0000000000..88d04e3d0d --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/learn/HorizonLearnUiTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.ui.features.learn + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.horizon.horizonui.molecules.Spinner +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class HorizonLearnUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testLoadingStateDisplaysSpinner() { + composeTestRule.setContent { + Spinner(modifier = Modifier.fillMaxSize()) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } + + @Test + fun testLearnTitleDisplays() { + composeTestRule.setContent { + androidx.compose.material3.Text("Learn") + } + + composeTestRule.onNodeWithText("Learn") + .assertIsDisplayed() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/moduleitemsequence/AssessmentUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/moduleitemsequence/AssessmentUiTest.kt new file mode 100644 index 0000000000..ca16cfe0ee --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/moduleitemsequence/AssessmentUiTest.kt @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.ui.features.moduleitemsequence + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.horizon.features.moduleitemsequence.content.assessment.AssessmentContentScreen +import com.instructure.horizon.features.moduleitemsequence.content.assessment.AssessmentUiState +import com.instructure.horizon.horizonui.platform.LoadingState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class AssessmentUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testStartQuizButtonDisplays() { + val uiState = AssessmentUiState( + assessmentName = "Math Quiz", + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + AssessmentContentScreen(uiState = uiState) + } + + composeTestRule.onNodeWithText("Start quiz") + .assertIsDisplayed() + .assertHasClickAction() + } + + @Test + fun testAssessmentNameDisplays() { + val uiState = AssessmentUiState( + assessmentName = "Final Exam", + showAssessmentDialog = true, + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + AssessmentContentScreen(uiState = uiState) + } + + composeTestRule.onNodeWithText("Final Exam") + .assertIsDisplayed() + } + + @Test + fun testLoadingStateDisplaysSpinner() { + val uiState = AssessmentUiState( + assessmentName = "Quiz", + loadingState = LoadingState(isLoading = true) + ) + + composeTestRule.setContent { + AssessmentContentScreen(uiState = uiState) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } + + @Test + fun testAssessmentDialogDisplaysWhenOpen() { + val uiState = AssessmentUiState( + assessmentName = "Quiz", + showAssessmentDialog = true, + urlToLoad = "https://example.com/quiz", + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + AssessmentContentScreen(uiState = uiState) + } + + composeTestRule.onNodeWithTag("AssessmentDialog") + .assertIsDisplayed() + } + + @Test + fun testAssessmentLoadingIndicatorDisplays() { + val uiState = AssessmentUiState( + assessmentName = "Quiz", + showAssessmentDialog = true, + assessmentLoading = true, + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + AssessmentContentScreen(uiState = uiState) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } + + @Test + fun testCompletionLoadingDisplays() { + val uiState = AssessmentUiState( + assessmentName = "Quiz", + assessmentCompletionLoading = true, + showAssessmentDialog = true, + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + AssessmentContentScreen(uiState = uiState) + } + + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + } + + @Test + fun testCloseAssessmentButtonDisplays() { + val uiState = AssessmentUiState( + assessmentName = "Quiz", + showAssessmentDialog = true, + urlToLoad = "https://example.com/quiz", + loadingState = LoadingState(isLoading = false) + ) + + composeTestRule.setContent { + AssessmentContentScreen(uiState = uiState) + } + + composeTestRule.onNodeWithContentDescription("Close") + .assertIsDisplayed() + .assertHasClickAction() + } +} diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/notification/HorizonNotificationUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/notification/HorizonNotificationUiTest.kt new file mode 100644 index 0000000000..b14b778d96 --- /dev/null +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/notification/HorizonNotificationUiTest.kt @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.ui.features.notification + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.navigation.compose.rememberNavController +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.horizon.features.notification.NotificationItem +import com.instructure.horizon.features.notification.NotificationItemCategory +import com.instructure.horizon.features.notification.NotificationScreen +import com.instructure.horizon.features.notification.NotificationUiState +import com.instructure.horizon.horizonui.molecules.StatusChipColor +import com.instructure.horizon.horizonui.platform.LoadingState +import com.instructure.pandautils.utils.localisedFormat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + +@RunWith(AndroidJUnit4::class) +class HorizonNotificationUiTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testLoadingState() { + val state = NotificationUiState( + screenState = LoadingState(isLoading = true), + ) + composeTestRule.setContent { + NotificationScreen(state, rememberNavController()) + } + + composeTestRule.onNodeWithText("Notifications") + .assertIsDisplayed() + composeTestRule.onNodeWithTag("LoadingSpinner") + .assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("Navigate back") + .assertIsDisplayed() + .assertHasClickAction() + } + + @Test + fun testContentStateTodayFormat() { + val state = NotificationUiState( + screenState = LoadingState(), + notificationItems = listOf( + NotificationItem( + category = NotificationItemCategory("Announcement", StatusChipColor.Sky), + courseLabel = "Biology 101", + title = "New Announcement", + date = Date(), + isRead = false, + deepLink = "" + ) + ) + ) + composeTestRule.setContent { + NotificationScreen(state, rememberNavController()) + } + + composeTestRule.onNodeWithText("Notifications") + .assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("Navigate back") + .assertIsDisplayed() + .assertHasClickAction() + + composeTestRule.onNodeWithText("Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("New Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Biology 101") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Today") + .assertIsDisplayed() + } + + @Test + fun testContentStateYesterdayFormat() { + val state = NotificationUiState( + screenState = LoadingState(), + notificationItems = listOf( + NotificationItem( + category = NotificationItemCategory("Announcement", StatusChipColor.Sky), + courseLabel = "Biology 101", + title = "New Announcement", + date = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -1) }.time, + isRead = false, + deepLink = "" + ) + ) + ) + composeTestRule.setContent { + NotificationScreen(state, rememberNavController()) + } + + composeTestRule.onNodeWithText("Notifications") + .assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("Navigate back") + .assertIsDisplayed() + .assertHasClickAction() + + composeTestRule.onNodeWithText("Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("New Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Biology 101") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Yesterday") + .assertIsDisplayed() + } + + @Test + fun testContentStateDayOfWeekFormat() { + val date = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -3) }.time + val state = NotificationUiState( + screenState = LoadingState(), + notificationItems = listOf( + NotificationItem( + category = NotificationItemCategory("Announcement", StatusChipColor.Sky), + courseLabel = "Biology 101", + title = "New Announcement", + date = date, + isRead = false, + deepLink = "" + ) + ) + ) + composeTestRule.setContent { + NotificationScreen(state, rememberNavController()) + } + val dateString = SimpleDateFormat("EEEE", Locale.getDefault()).format(date) + + composeTestRule.onNodeWithText("Notifications") + .assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("Navigate back") + .assertIsDisplayed() + .assertHasClickAction() + + composeTestRule.onNodeWithText("Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("New Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Biology 101") + .assertIsDisplayed() + composeTestRule.onNodeWithText(dateString) + .assertIsDisplayed() + } + + @Test + fun testContentStateDateFormat() { + val date = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -7) }.time + val state = NotificationUiState( + screenState = LoadingState(), + notificationItems = listOf( + NotificationItem( + category = NotificationItemCategory("Announcement", StatusChipColor.Sky), + courseLabel = "Biology 101", + title = "New Announcement", + date = date, + isRead = false, + deepLink = "" + ) + ) + ) + composeTestRule.setContent { + NotificationScreen(state, rememberNavController()) + } + val dateString = date.localisedFormat("MMM dd, yyyy") + + composeTestRule.onNodeWithText("Notifications") + .assertIsDisplayed() + composeTestRule.onNodeWithContentDescription("Navigate back") + .assertIsDisplayed() + .assertHasClickAction() + + composeTestRule.onNodeWithText("Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("New Announcement") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Biology 101") + .assertIsDisplayed() + composeTestRule.onNodeWithText(dateString) + .assertIsDisplayed() + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/HorizonActivity.kt b/libs/horizon/src/main/java/com/instructure/horizon/HorizonActivity.kt index 0870bd006f..9d8dbf869c 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/HorizonActivity.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/HorizonActivity.kt @@ -34,6 +34,8 @@ import androidx.navigation.NavDeepLinkRequest import androidx.navigation.NavHostController import androidx.navigation.NavOptions import androidx.navigation.compose.rememberNavController +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.horizon.horizonui.HorizonTheme import com.instructure.horizon.navigation.HorizonNavigation import com.instructure.pandautils.base.BaseCanvasActivity @@ -43,6 +45,7 @@ import com.instructure.pandautils.utils.AppTheme import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.Utils import com.instructure.pandautils.utils.ViewStyler import com.instructure.pandautils.utils.WebViewAuthenticator import com.instructure.pandautils.utils.getActivityOrNull @@ -151,4 +154,21 @@ class HorizonActivity : BaseCanvasActivity() { } return flag } + + /** + * ONLY USE FOR UI TESTING + * Skips the traditional login process by directly setting the domain, token, and user info. + */ + fun loginWithToken(token: String, domain: String, user: User) { + ApiPrefs.accessToken = token + ApiPrefs.domain = domain + ApiPrefs.user = user + ApiPrefs.canvasCareerView = true + ApiPrefs.userAgent = Utils.generateUserAgent(this, Const.STUDENT_USER_AGENT) + finish() + val intent = Intent(this, HorizonActivity::class.java).apply { + intent?.extras?.let { putExtras(it) } + } + startActivity(intent) + } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/account/filepreview/PdfPreview.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/account/filepreview/PdfPreview.kt index 8e144427f0..fc3443b886 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/account/filepreview/PdfPreview.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/account/filepreview/PdfPreview.kt @@ -25,10 +25,8 @@ import com.pspdfkit.configuration.activity.UserInterfaceViewMode import com.pspdfkit.configuration.page.PageScrollDirection import com.pspdfkit.configuration.page.PageScrollMode import com.pspdfkit.jetpack.compose.interactors.rememberDocumentState -import com.pspdfkit.jetpack.compose.utilities.ExperimentalPSPDFKitApi import com.pspdfkit.jetpack.compose.views.DocumentView -@OptIn(ExperimentalPSPDFKitApi::class) @Composable fun PdfPreview( documentUri: Uri, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatRepository.kt index e7599ecf07..0f792ca642 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatRepository.kt @@ -16,9 +16,9 @@ */ package com.instructure.horizon.features.aiassistant.chat -import com.instructure.canvasapi2.managers.CedarApiManager -import com.instructure.canvasapi2.managers.DocumentSource -import com.instructure.canvasapi2.managers.PineApiManager +import com.instructure.canvasapi2.managers.graphql.horizon.cedar.CedarApiManager +import com.instructure.canvasapi2.managers.graphql.horizon.pine.DocumentSource +import com.instructure.canvasapi2.managers.graphql.horizon.pine.PineApiManager import com.instructure.cedar.type.DocumentBlock import com.instructure.pine.type.MessageInput import javax.inject.Inject diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/flashcard/AiAssistFlashcardRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/flashcard/AiAssistFlashcardRepository.kt index 0c66799c4f..14239c4b47 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/flashcard/AiAssistFlashcardRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/flashcard/AiAssistFlashcardRepository.kt @@ -16,7 +16,7 @@ */ package com.instructure.horizon.features.aiassistant.flashcard -import com.instructure.canvasapi2.managers.CedarApiManager +import com.instructure.canvasapi2.managers.graphql.horizon.cedar.CedarApiManager import com.instructure.cedar.type.DocumentBlock import kotlinx.serialization.json.Json import javax.inject.Inject diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/quiz/AiAssistQuizRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/quiz/AiAssistQuizRepository.kt index 09791a92e2..7f6e7ab60d 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/quiz/AiAssistQuizRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/quiz/AiAssistQuizRepository.kt @@ -16,8 +16,8 @@ */ package com.instructure.horizon.features.aiassistant.quiz -import com.instructure.canvasapi2.managers.CedarApiManager -import com.instructure.canvasapi2.managers.GeneratedQuiz +import com.instructure.canvasapi2.managers.graphql.horizon.cedar.CedarApiManager +import com.instructure.canvasapi2.managers.graphql.horizon.cedar.GeneratedQuiz import javax.inject.Inject class AiAssistQuizRepository @Inject constructor( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardCard.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardCard.kt new file mode 100644 index 0000000000..b674f62722 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardCard.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius +import com.instructure.horizon.horizonui.foundation.HorizonElevation +import com.instructure.horizon.horizonui.foundation.horizonShadow +import com.instructure.pandautils.compose.modifiers.conditional + +@Composable +fun DashboardCard( + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + content: @Composable () -> Unit +) { + Box(modifier = modifier + .horizonShadow(HorizonElevation.level4, shape = HorizonCornerRadius.level4) + .background(color = HorizonColors.Surface.cardPrimary(), shape = HorizonCornerRadius.level4) + .conditional(onClick != null) { + clickable(onClick = { onClick?.invoke() }) + } + ) { + content() + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardEventHandler.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardEventHandler.kt new file mode 100644 index 0000000000..4632c90caf --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardEventHandler.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.horizon.features.dashboard + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import javax.inject.Inject +import javax.inject.Singleton + +sealed interface DashboardEvent { + data object DashboardRefresh : DashboardEvent + data object ProgressRefresh : DashboardEvent + data object AnnouncementRefresh : DashboardEvent + data class ShowSnackbar(val message: String) : DashboardEvent +} + +@Singleton +class DashboardEventHandler @Inject constructor() { + + private val _events = MutableSharedFlow(replay = 0) + val events = _events.asSharedFlow() + + suspend fun postEvent(event: DashboardEvent) { + _events.emit(event) + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardItemState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardItemState.kt new file mode 100644 index 0000000000..76a0f49982 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardItemState.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard + +enum class DashboardItemState { + LOADING, ERROR, SUCCESS +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardRepository.kt index 7e9037d5d9..74c5f09f30 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardRepository.kt @@ -1,59 +1,30 @@ /* * Copyright (C) 2025 - present Instructure, Inc. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ package com.instructure.horizon.features.dashboard -import com.instructure.canvasapi2.apis.EnrollmentAPI -import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.apis.UnreadCountAPI import com.instructure.canvasapi2.builders.RestParams -import com.instructure.canvasapi2.managers.DashboardContent -import com.instructure.canvasapi2.managers.HorizonGetCoursesManager -import com.instructure.canvasapi2.managers.graphql.JourneyApiManager -import com.instructure.canvasapi2.managers.graphql.Program -import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.canvasapi2.models.ModuleObject -import com.instructure.canvasapi2.utils.ApiPrefs -import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.models.UnreadNotificationCount import javax.inject.Inject class DashboardRepository @Inject constructor( - private val horizonGetCoursesManager: HorizonGetCoursesManager, - private val moduleApi: ModuleAPI.ModuleInterface, - private val apiPrefs: ApiPrefs, - private val enrollmentApi: EnrollmentAPI.EnrollmentInterface, - private val journeyApiManager: JourneyApiManager, + private val unreadCountApi: UnreadCountAPI.UnreadCountsInterface, ) { - suspend fun getDashboardContent(forceNetwork: Boolean): DataResult { - return horizonGetCoursesManager.getDashboardContent(apiPrefs.user?.id ?: -1, forceNetwork) - } - - suspend fun getFirstPageModulesWithItems(courseId: Long, forceNetwork: Boolean): DataResult> { - val params = RestParams(isForceReadFromNetwork = forceNetwork) - return moduleApi.getFirstPageModulesWithItems( - CanvasContext.Type.COURSE.apiString, - courseId, - params, - includes = listOf("estimated_durations") - ) - } - - suspend fun acceptInvite(courseId: Long, enrollmentId: Long) { - return enrollmentApi.acceptInvite(courseId, enrollmentId, RestParams()).dataOrThrow - } - - suspend fun getPrograms(forceNetwork: Boolean = false): List { - return journeyApiManager.getPrograms(forceNetwork) + suspend fun getUnreadCounts(forceNetwork: Boolean): List { + return unreadCountApi.getNotificationsCount(RestParams(isForceReadFromNetwork = forceNetwork)).dataOrNull.orEmpty() } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt index eaab21d171..57b515c1d0 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt @@ -22,25 +22,24 @@ import android.content.pm.PackageManager import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -51,176 +50,216 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.LinkAnnotation -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextLinkStyles -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.withLink import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.navigation.NavController -import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.horizon.R -import com.instructure.horizon.features.home.HomeNavigationRoute -import com.instructure.horizon.features.moduleitemsequence.SHOULD_REFRESH_DASHBOARD +import com.instructure.horizon.features.dashboard.widget.announcement.DashboardAnnouncementBannerWidget +import com.instructure.horizon.features.dashboard.widget.course.DashboardCourseSection +import com.instructure.horizon.features.dashboard.widget.myprogress.DashboardMyProgressWidget +import com.instructure.horizon.features.dashboard.widget.skillhighlights.DashboardSkillHighlightsWidget +import com.instructure.horizon.features.dashboard.widget.skilloverview.DashboardSkillOverviewWidget +import com.instructure.horizon.features.dashboard.widget.timespent.DashboardTimeSpentWidget +import com.instructure.horizon.horizonui.animation.shimmerEffect import com.instructure.horizon.horizonui.foundation.HorizonColors -import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius import com.instructure.horizon.horizonui.foundation.HorizonElevation import com.instructure.horizon.horizonui.foundation.HorizonSpace -import com.instructure.horizon.horizonui.foundation.HorizonTypography import com.instructure.horizon.horizonui.foundation.SpaceSize -import com.instructure.horizon.horizonui.molecules.Button -import com.instructure.horizon.horizonui.molecules.ButtonColor +import com.instructure.horizon.horizonui.molecules.Badge +import com.instructure.horizon.horizonui.molecules.BadgeContent +import com.instructure.horizon.horizonui.molecules.BadgeType import com.instructure.horizon.horizonui.molecules.IconButton import com.instructure.horizon.horizonui.molecules.IconButtonColor -import com.instructure.horizon.horizonui.molecules.LoadingButton -import com.instructure.horizon.horizonui.molecules.ProgressBar -import com.instructure.horizon.horizonui.organisms.Alert -import com.instructure.horizon.horizonui.organisms.AlertType -import com.instructure.horizon.horizonui.organisms.cards.LearningObjectCard -import com.instructure.horizon.horizonui.organisms.cards.LearningObjectCardState -import com.instructure.horizon.horizonui.platform.LoadingStateWrapper +import com.instructure.horizon.horizonui.organisms.AnimatedHorizontalPager +import com.instructure.horizon.horizonui.organisms.CollapsableHeaderScreen import com.instructure.horizon.navigation.MainNavigationRoute -import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.compose.modifiers.conditional +import kotlinx.coroutines.flow.MutableStateFlow @Composable fun DashboardScreen(uiState: DashboardUiState, mainNavController: NavHostController, homeNavController: NavHostController) { + val snackbarHostState = remember { SnackbarHostState() } - val parentEntry = remember(mainNavController.currentBackStackEntry) { mainNavController.getBackStackEntry("home") } - val savedStateHandle = parentEntry.savedStateHandle + var shouldRefresh by rememberSaveable { mutableStateOf(false) } - val refreshFlow = remember { savedStateHandle.getStateFlow(SHOULD_REFRESH_DASHBOARD, false) } - - val shouldRefresh by refreshFlow.collectAsState() + /* + Using a list of booleans to represent each refreshing component. + Components get the `shouldRefresh` flag to start refreshing on pull-to-refresh. + Each component append the `refreshStateFlow` with `true` when starting to refresh and remove it when done. + If any component is refreshing, the dashboard shows the refreshing indicator. + */ + val refreshStateFlow = remember { MutableStateFlow(emptyList()) } + val refreshState by refreshStateFlow.collectAsState() NotificationPermissionRequest() LaunchedEffect(shouldRefresh) { if (shouldRefresh) { - uiState.loadingState.onRefresh() - savedStateHandle[SHOULD_REFRESH_DASHBOARD] = false + shouldRefresh = false + } + } + + LaunchedEffect(uiState.externalShouldRefresh) { + if (uiState.externalShouldRefresh) { + shouldRefresh = true + uiState.updateExternalShouldRefresh(false) + } + } + + LaunchedEffect(uiState.snackbarMessage) { + if (uiState.snackbarMessage != null) { + val result = snackbarHostState.showSnackbar( + message = uiState.snackbarMessage, + ) + if (result == SnackbarResult.Dismissed) { + uiState.onSnackbarDismiss() + } } } - Scaffold(containerColor = HorizonColors.Surface.pagePrimary()) { paddingValues -> - val spinnerColor = - if (ThemePrefs.isThemeApplied) HorizonColors.Surface.institution() else HorizonColors.Surface.inverseSecondary() - LoadingStateWrapper(loadingState = uiState.loadingState, spinnerColor = spinnerColor, modifier = Modifier.padding(paddingValues)) { - if (uiState.coursesUiState.isEmpty() && uiState.invitesUiState.isEmpty()) { - Column( + Scaffold( + containerColor = HorizonColors.Surface.pagePrimary(), + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { paddingValues -> + val pullToRefreshState = rememberPullToRefreshState() + val isRefreshing = refreshState.any { it } + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { shouldRefresh = true }, + state = pullToRefreshState, + indicator = { + Indicator( modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp) - .verticalScroll(rememberScrollState()) - .height(IntrinsicSize.Max) - ) { - HomeScreenTopBar(uiState, mainNavController, modifier = Modifier.height(56.dp)) - HorizonSpace(SpaceSize.SPACE_24) - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text( - stringResource(R.string.dashboard_emptyMessage), - style = HorizonTypography.h3, - textAlign = TextAlign.Center + .align(Alignment.TopCenter) + .padding(top = 56.dp), + isRefreshing = isRefreshing, + containerColor = HorizonColors.Surface.pageSecondary(), + color = HorizonColors.Surface.institution(), + state = pullToRefreshState + ) + } + ){ + val scrollState = rememberScrollState() + CollapsableHeaderScreen( + modifier = Modifier.padding(paddingValues), + headerContent = { + Column( + modifier = Modifier.conditional(scrollState.canScrollBackward) { + shadow( + elevation = HorizonElevation.level3, + spotColor = Color.Transparent, + ) + } + ) { + HomeScreenTopBar( + uiState, + mainNavController, + modifier = Modifier + .height(56.dp) + .padding(bottom = 12.dp) ) } - } - } else { - LazyColumn( - contentPadding = PaddingValues(start = 24.dp, end = 24.dp), - content = { - item { - HomeScreenTopBar(uiState, mainNavController, modifier = Modifier.height(56.dp)) - HorizonSpace(SpaceSize.SPACE_24) - } - items(uiState.invitesUiState) { inviteItem -> - Alert( - stringResource(R.string.dashboard_courseInvite, inviteItem.courseName), - alertType = AlertType.Info, - buttons = { - LoadingButton( - label = stringResource(R.string.dashboard_courseInviteAccept), - contentAlignment = Alignment.CenterStart, - color = ButtonColor.Black, - onClick = inviteItem.onAccept, - loading = inviteItem.acceptLoading + }, + bodyContent = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .verticalScroll(scrollState) + ) { + HorizonSpace(SpaceSize.SPACE_12) + DashboardAnnouncementBannerWidget( + mainNavController, + homeNavController, + shouldRefresh, + refreshStateFlow + ) + DashboardCourseSection( + mainNavController, + homeNavController, + shouldRefresh, + refreshStateFlow + ) + HorizonSpace(SpaceSize.SPACE_16) + val pagerState = rememberPagerState{ 3 } + AnimatedHorizontalPager( + pagerState, + sizeAnimationRange = 0f, + contentPadding = PaddingValues(horizontal = 24.dp), + pageSpacing = 12.dp, + verticalAlignment = Alignment.CenterVertically, + ) { index, modifier -> + when (index) { + 0 -> { + DashboardMyProgressWidget( + shouldRefresh, + refreshStateFlow, + modifier.padding(bottom = 16.dp) ) - }, - onDismiss = if (inviteItem.acceptLoading) null else inviteItem.onDismiss - ) - HorizonSpace(SpaceSize.SPACE_16) - } - items(uiState.programsUiState) { program -> - DashboardProgramItem(program) { - homeNavController.navigate(HomeNavigationRoute.Learn.withProgram(program.id)) { - popUpTo(homeNavController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = false } - } - } - itemsIndexed(uiState.coursesUiState) { index, courseItem -> - DashboardCourseItem(courseItem, onClick = { - mainNavController.navigate( - MainNavigationRoute.ModuleItemSequence( - courseItem.courseId, - courseItem.nextModuleItemId + 1 -> { + DashboardTimeSpentWidget( + shouldRefresh, + refreshStateFlow, + modifier.padding(bottom = 16.dp) + ) + } + 2 -> { + DashboardSkillOverviewWidget( + homeNavController, + shouldRefresh, + refreshStateFlow, + modifier.padding(bottom = 16.dp) ) - ) - }, onCourseClick = { - homeNavController.navigate(HomeNavigationRoute.Learn.withCourse(courseItem.courseId)) { - popUpTo(homeNavController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = false } - }, onProgramClick = { programId -> - homeNavController.navigate(HomeNavigationRoute.Learn.withProgram(programId)) { - popUpTo(homeNavController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = false + else -> { + } - }) - if (index < uiState.coursesUiState.size - 1) { - HorizonSpace(SpaceSize.SPACE_48) } } - }) - } + DashboardSkillHighlightsWidget( + homeNavController, + shouldRefresh, + refreshStateFlow + ) + HorizonSpace(SpaceSize.SPACE_24) + } + } + ) } } } @Composable -private fun HomeScreenTopBar(uiState: DashboardUiState, mainNavController: NavController, modifier: Modifier = Modifier) { - Row(verticalAlignment = Alignment.Bottom, modifier = modifier) { +private fun HomeScreenTopBar(uiState: DashboardUiState, mainNavController: NavController, modifier: Modifier = Modifier +) { + Row( + verticalAlignment = Alignment.Bottom, + modifier = modifier.padding(horizontal = 24.dp) + ) { GlideImage( model = uiState.logoUrl, contentScale = ContentScale.Fit, modifier = Modifier .weight(1f) - .heightIn(max = 44.dp), + .heightIn(max = 44.dp) + .shimmerEffect(uiState.logoUrl.isEmpty()), contentDescription = stringResource(R.string.a11y_institutionLogoContentDescription), ) Spacer(modifier = Modifier.weight(1f)) IconButton( iconRes = R.drawable.menu_book_notebook, + contentDescription = stringResource(R.string.a11y_dashboardNotebookButtonContentDescription), onClick = { mainNavController.navigate(MainNavigationRoute.Notebook.route) }, @@ -230,119 +269,40 @@ private fun HomeScreenTopBar(uiState: DashboardUiState, mainNavController: NavCo HorizonSpace(SpaceSize.SPACE_8) IconButton( iconRes = R.drawable.notifications, + contentDescription = stringResource(R.string.a11y_dashboardNotificationsContentDescription), onClick = { mainNavController.navigate(MainNavigationRoute.Notification.route) }, elevation = HorizonElevation.level4, - color = IconButtonColor.Inverse + color = IconButtonColor.Inverse, + badge = if (uiState.unreadCountState.unreadNotifications > 0) { + { + Badge( + content = BadgeContent.Color, + type = BadgeType.Inverse + ) + } + } else null ) HorizonSpace(SpaceSize.SPACE_8) IconButton( iconRes = R.drawable.mail, + contentDescription = stringResource(R.string.a11y_dashboardInboxContentDescription), onClick = { mainNavController.navigate(MainNavigationRoute.Inbox.route) }, elevation = HorizonElevation.level4, - color = IconButtonColor.Inverse + color = IconButtonColor.Inverse, + badge = if (uiState.unreadCountState.unreadConversations > 0) { + { + Badge( + content = BadgeContent.Color, + type = BadgeType.Inverse + ) + } + } else null ) } } -@Composable -private fun DashboardProgramItem( - programUiState: DashboardProgramUiState, - onProgramClick: () -> Unit -) { - Column { - Text(text = programUiState.name, style = HorizonTypography.h2) - HorizonSpace(SpaceSize.SPACE_12) - Text(text = stringResource(R.string.dashboard_viewProgram), style = HorizonTypography.p1) - HorizonSpace(SpaceSize.SPACE_24) - Button(label = stringResource(R.string.dashboard_viewProgramButton), color = ButtonColor.Institution, onClick = onProgramClick) - HorizonSpace(SpaceSize.SPACE_24) - } -} - -@Composable -private fun DashboardCourseItem( - courseItem: DashboardCourseUiState, - onClick: () -> Unit, - onCourseClick: () -> Unit, - modifier: Modifier = Modifier, - onProgramClick: (String) -> Unit = {} -) { - Column(modifier) { - Column( - Modifier - .clip(HorizonCornerRadius.level1_5) - .clickable { - onCourseClick() - }) { - if (courseItem.parentPrograms.isNotEmpty()) { - ProgramsText(programs = courseItem.parentPrograms, onProgramClick = onProgramClick) - HorizonSpace(SpaceSize.SPACE_12) - } - Text(text = courseItem.courseName, style = HorizonTypography.h1) - HorizonSpace(SpaceSize.SPACE_12) - ProgressBar(progress = courseItem.courseProgress) - HorizonSpace(SpaceSize.SPACE_36) - } - if (courseItem.completed) { - Text(text = stringResource(R.string.dashboard_courseCompleted), style = HorizonTypography.h3) - HorizonSpace(SpaceSize.SPACE_12) - Text(text = stringResource(R.string.dashboard_courseCompletedDescription), style = HorizonTypography.p1) - } else { - Text(text = stringResource(R.string.dashboard_resumeLearning), style = HorizonTypography.h3) - HorizonSpace(SpaceSize.SPACE_12) - LearningObjectCard( - LearningObjectCardState( - moduleTitle = courseItem.nextModuleName.orEmpty(), - learningObjectTitle = courseItem.nextModuleItemName.orEmpty(), - progressLabel = courseItem.progressLabel, - remainingTime = courseItem.remainingTime, - dueDate = courseItem.dueDate, - learningObjectType = courseItem.learningObjectType, - onClick = onClick - ) - ) - } - HorizonSpace(SpaceSize.SPACE_24) - } -} - -@Composable -private fun ProgramsText( - programs: List, - onProgramClick: (String) -> Unit -) { - val programsAnnotated = buildAnnotatedString { - programs.forEachIndexed { i, program -> - if (i > 0) append(", ") - withLink( - LinkAnnotation.Clickable( - tag = program.programId, - styles = TextLinkStyles( - style = SpanStyle(textDecoration = TextDecoration.Underline) - ), - linkInteractionListener = { _ -> onProgramClick(program.programId) } - ) - ) { - append(program.programName) - } - } - } - - // String resource can't work with annotated string so we need a temporary placeholder - val template = stringResource(R.string.learnScreen_partOfProgram, "__PROGRAMS__") - - val fullText = buildAnnotatedString { - val parts = template.split("__PROGRAMS__") - append(parts[0]) - append(programsAnnotated) - if (parts.size > 1) append(parts[1]) - } - - Text(style = HorizonTypography.p1, text = fullText) -} - @Composable private fun NotificationPermissionRequest() { val context = LocalContext.current diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardUiState.kt index 955f560071..21cae2251e 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardUiState.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardUiState.kt @@ -15,47 +15,16 @@ */ package com.instructure.horizon.features.dashboard -import com.instructure.horizon.horizonui.platform.LoadingState -import com.instructure.horizon.model.LearningObjectType -import java.util.Date - data class DashboardUiState( val logoUrl: String = "", - val programsUiState: List = emptyList(), - val coursesUiState: List = emptyList(), - val invitesUiState: List = emptyList(), - val loadingState: LoadingState = LoadingState(), -) - -data class DashboardCourseUiState( - val courseId: Long, - val courseName: String, - val courseProgress: Double, - val completed: Boolean = false, - val nextModuleName: String? = null, - val nextModuleItemName: String? = null, - val nextModuleItemId: Long? = null, - val progressLabel: String? = null, - val remainingTime: String? = null, - val learningObjectType: LearningObjectType? = null, - val dueDate: Date? = null, - val parentPrograms: List = emptyList() -) - -data class DashboardCourseProgram( - val programName: String = "", - val programId: String = "", -) - -data class CourseInviteUiState( - val courseId: Long, - val courseName: String, - val onAccept: () -> Unit, - val onDismiss: () -> Unit, - val acceptLoading: Boolean = false, + val externalShouldRefresh: Boolean = false, + val updateExternalShouldRefresh: (Boolean) -> Unit = {}, + val unreadCountState: DashboardUnreadState = DashboardUnreadState(), + val snackbarMessage: String? = null, + val onSnackbarDismiss: () -> Unit = {}, ) -data class DashboardProgramUiState( - val id: String, - val name: String, +data class DashboardUnreadState( + val unreadConversations: Int = 0, + val unreadNotifications: Int = 0, ) \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardViewModel.kt index 538adcfc9b..6a84876469 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardViewModel.kt @@ -15,247 +15,98 @@ */ package com.instructure.horizon.features.dashboard -import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.instructure.canvasapi2.managers.CourseWithProgress -import com.instructure.canvasapi2.managers.DashboardCourse -import com.instructure.canvasapi2.managers.graphql.Program -import com.instructure.canvasapi2.models.ModuleItem -import com.instructure.canvasapi2.models.ModuleObject import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch -import com.instructure.horizon.R -import com.instructure.horizon.horizonui.platform.LoadingState -import com.instructure.horizon.model.LearningObjectType -import com.instructure.journey.type.ProgramProgressCourseEnrollmentStatus import com.instructure.pandautils.utils.ThemePrefs -import com.instructure.pandautils.utils.formatIsoDuration import com.instructure.pandautils.utils.poll import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class DashboardViewModel @Inject constructor( private val dashboardRepository: DashboardRepository, - @ApplicationContext private val context: Context, - private val themePrefs: ThemePrefs + private val themePrefs: ThemePrefs, + private val dashboardEventHandler: DashboardEventHandler ) : ViewModel() { - private val _uiState = - MutableStateFlow(DashboardUiState(loadingState = LoadingState(onRefresh = ::refresh, onSnackbarDismiss = ::dismissSnackbar))) + private val _uiState = MutableStateFlow(DashboardUiState(onSnackbarDismiss = ::dismissSnackbar, updateExternalShouldRefresh = ::updateExternalShouldRefresh)) val uiState = _uiState.asStateFlow() init { viewModelScope.tryLaunch { - _uiState.update { it.copy(loadingState = it.loadingState.copy(isLoading = true)) } - loadData(forceNetwork = false) - _uiState.update { it.copy(loadingState = it.loadingState.copy(isLoading = false)) } + loadUnreadCount() + loadLogo() } catch { - _uiState.update { it.copy(loadingState = it.loadingState.copy(isLoading = false)) } + } - } - fun refresh() { - viewModelScope.tryLaunch { - _uiState.update { it.copy(loadingState = it.loadingState.copy(isRefreshing = true)) } - loadData(forceNetwork = true) - _uiState.update { it.copy(loadingState = it.loadingState.copy(isRefreshing = false)) } - } catch { - _uiState.update { it.copy(loadingState = it.loadingState.copy(isRefreshing = false)) } + viewModelScope.launch { + dashboardEventHandler.events.collect { event -> + when (event) { + is DashboardEvent.DashboardRefresh -> { + _uiState.update { it.copy(externalShouldRefresh = true) } + refresh() + } + is DashboardEvent.ShowSnackbar -> showSnackbar(event.message) + else -> { /* No-op */ } + } + } } } - private suspend fun loadData(forceNetwork: Boolean) { + private suspend fun loadLogo() { // We need to poll for the logo URL because the Dashboard already starts to load when the canvas theme is not yet applied at the first launch. poll( pollInterval = 50, maxAttempts = 10, block = { _uiState.update { it.copy(logoUrl = themePrefs.mobileLogoUrl) } }, - validate = { themePrefs.mobileLogoUrl.isNotEmpty() }) - val dashboardContent = dashboardRepository.getDashboardContent(forceNetwork = forceNetwork) - if (dashboardContent.isSuccess) { - val programs = try { - dashboardRepository.getPrograms(forceNetwork = forceNetwork) - } catch (e: Exception) { - emptyList() - } - val coursesResult = dashboardContent.dataOrThrow.courses - val courseUiStates = coursesResult.map { course -> - viewModelScope.async { - mapCourse(course, programs, forceNetwork) - } - }.awaitAll().filterNotNull() - val programsUiState = programs - .filter { program -> program.sortedRequirements.none { it.enrollmentStatus == ProgramProgressCourseEnrollmentStatus.ENROLLED } } - .map { DashboardProgramUiState(it.id, it.name) } - val inviteResults = dashboardContent.dataOrThrow.courseInvites - val invites = inviteResults.map { courseInvite -> - CourseInviteUiState(courseId = courseInvite.courseId, courseName = courseInvite.courseName, onAccept = { - updateAcceptLoadingForInvite(courseInvite.courseId, true) - viewModelScope.tryLaunch { - dashboardRepository.acceptInvite(courseInvite.courseId, courseInvite.enrollmentId) - refresh() - } catch { - _uiState.update { it.copy(loadingState = it.loadingState.copy(snackbarMessage = context.getString(R.string.dashboard_courseInviteFailed))) } - updateAcceptLoadingForInvite(courseInvite.courseId, false) - } - }, onDismiss = { - dismissInvite(courseInvite.courseId) - }) - } - _uiState.update { - it.copy( - programsUiState = programsUiState, - coursesUiState = courseUiStates, - invitesUiState = invites, - loadingState = it.loadingState.copy(isError = false) - ) - } - } else { - handleError() - } + validate = { themePrefs.mobileLogoUrl.isNotEmpty() } + ) } - private suspend fun mapCourse( - dashboardCourse: DashboardCourse, - programs: List, - forceNetwork: Boolean - ): DashboardCourseUiState? { - val nextModuleId = dashboardCourse.nextUpModuleId - val nextModuleItemId = dashboardCourse.nextUpModuleItemId - val parentPrograms = - programs - .filter { program -> program.sortedRequirements.any { it.courseId == dashboardCourse.course.courseId } } - .map { DashboardCourseProgram(it.name, it.id) } - return if (nextModuleId != null && nextModuleItemId != null) { - createCourseUiState(dashboardCourse, parentPrograms) - } else if (dashboardCourse.course.progress < 100.0) { - - val modules = dashboardRepository.getFirstPageModulesWithItems( - dashboardCourse.course.courseId, - forceNetwork = forceNetwork - ) - - if (modules.isSuccess) { - val nextModuleItemResult = modules.dataOrThrow.flatMap { module -> module.items }.firstOrNull() - val nextModuleResult = modules.dataOrThrow.find { module -> module.id == nextModuleItemResult?.moduleId } - - if (nextModuleItemResult == null || nextModuleResult == null) { - return null - } - createCourseUiState(dashboardCourse.course, nextModuleResult, nextModuleItemResult, parentPrograms) - } else { - handleError() - null - } - } else if (dashboardCourse.course.progress == 100.0) { - DashboardCourseUiState( - courseId = dashboardCourse.course.courseId, - courseName = dashboardCourse.course.courseName, - courseProgress = dashboardCourse.course.progress, - completed = true, - progressLabel = getProgressLabel(dashboardCourse.course.progress), - parentPrograms = parentPrograms + private suspend fun loadUnreadCount() { + val unreadCounts = dashboardRepository.getUnreadCounts(true) + val unreadConversations = unreadCounts.firstOrNull { it.type == "Conversation" }?.unreadCount ?: 0 + val unreadNotifications = unreadCounts.filter { it.type == "Message" }.sumOf { it.unreadCount } + _uiState.update { + it.copy( + unreadCountState = DashboardUnreadState( + unreadConversations = unreadConversations, + unreadNotifications = unreadNotifications + ) ) - } else { - handleError() - null } } - private fun createCourseUiState( - dashboardCourse: DashboardCourse, parentPrograms: List - ) = DashboardCourseUiState( - courseId = dashboardCourse.course.courseId, - courseName = dashboardCourse.course.courseName, - courseProgress = dashboardCourse.course.progress, - nextModuleName = dashboardCourse.nextUpModuleTitle ?: "", - nextModuleItemId = dashboardCourse.nextUpModuleItemId, - nextModuleItemName = dashboardCourse.nextUpModuleItemTitle ?: "", - progressLabel = getProgressLabel(dashboardCourse.course.progress), - remainingTime = dashboardCourse.nextModuleItemEstimatedDuration?.formatIsoDuration(context), - learningObjectType = if (dashboardCourse.isNewQuiz) LearningObjectType.ASSESSMENT else LearningObjectType.fromApiString( - dashboardCourse.nextModuleItemType.orEmpty() - ), - dueDate = dashboardCourse.nextModuleItemDueDate, - parentPrograms = parentPrograms - ) - - private fun createCourseUiState( - course: CourseWithProgress, - nextModule: ModuleObject?, - nextModuleItem: ModuleItem, - parentPrograms: List - ) = DashboardCourseUiState( - courseId = course.courseId, - courseName = course.courseName, - courseProgress = course.progress, - nextModuleName = nextModule?.name ?: "", - nextModuleItemId = nextModuleItem.id, - nextModuleItemName = nextModuleItem.title ?: "", - progressLabel = getProgressLabel(course.progress), - remainingTime = nextModuleItem.estimatedDuration?.formatIsoDuration(context), - learningObjectType = if (nextModuleItem.quizLti) LearningObjectType.ASSESSMENT else LearningObjectType.fromApiString(nextModuleItem.type.orEmpty()), - dueDate = nextModuleItem.moduleDetails?.dueDate, - parentPrograms = parentPrograms - ) - - private fun getProgressLabel(progress: Double): String { - return when (progress) { - 0.0 -> { - context.getString(R.string.learningObject_pillStatusNotStarted).uppercase() - } - - 100.0 -> { - context.getString(R.string.learningObject_pillStatusCompleted).uppercase() - } + fun refresh() { + viewModelScope.tryLaunch { + loadUnreadCount() + } catch { - else -> { - context.getString(R.string.learningObject_pillStatusInProgress).uppercase() - } } } - private fun handleError() { + private fun showSnackbar(message: String) { _uiState.update { - if (it.coursesUiState.isEmpty()) { - it.copy(loadingState = it.loadingState.copy(isError = true)) - } else { - it.copy(loadingState = it.loadingState.copy(snackbarMessage = context.getString(R.string.errorOccurred))) - } + it.copy(snackbarMessage = message) } } private fun dismissSnackbar() { _uiState.update { - it.copy(loadingState = it.loadingState.copy(snackbarMessage = null)) - } - } - - private fun updateAcceptLoadingForInvite(courseId: Long, isLoading: Boolean) { - _uiState.update { currentState -> - val updatedInvites = currentState.invitesUiState.map { invite -> - if (invite.courseId == courseId) { - invite.copy(acceptLoading = isLoading) - } else { - invite - } - } - currentState.copy(invitesUiState = updatedInvites) + it.copy(snackbarMessage = null) } } - private fun dismissInvite(courseId: Long) { - _uiState.update { currentState -> - val updatedInvites = currentState.invitesUiState.filterNot { it.courseId == courseId } - currentState.copy(invitesUiState = updatedInvites) + private fun updateExternalShouldRefresh(value: Boolean) { + _uiState.update { + it.copy(externalShouldRefresh = value) } } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/DashboardPaginatedWidgetCard.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/DashboardPaginatedWidgetCard.kt new file mode 100644 index 0000000000..51cc85b63c --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/DashboardPaginatedWidgetCard.kt @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardCard +import com.instructure.horizon.horizonui.animation.shimmerEffect +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonSpace +import com.instructure.horizon.horizonui.foundation.HorizonTypography +import com.instructure.horizon.horizonui.foundation.SpaceSize +import com.instructure.horizon.horizonui.molecules.StatusChip +import com.instructure.horizon.horizonui.molecules.StatusChipColor +import com.instructure.horizon.horizonui.molecules.StatusChipState +import com.instructure.horizon.horizonui.organisms.AnimatedHorizontalPager +import com.instructure.pandautils.utils.localisedFormat +import java.util.Date + + +@Composable +fun DashboardPaginatedWidgetCard( + state: DashboardPaginatedWidgetCardState, + mainNavController: NavHostController, + homeNavController: NavHostController, + modifier: Modifier = Modifier, +) { + val pagerState = rememberPagerState(pageCount = { state.items.size }) + + if (state.items.isNotEmpty()) { + + AnimatedHorizontalPager( + pagerState, + modifier, + sizeAnimationRange = 0f, + contentPadding = PaddingValues(horizontal = 24.dp), + pageSpacing = 12.dp, + verticalAlignment = Alignment.CenterVertically, + ) { index, modifier -> + DashboardCard( + modifier = modifier.padding(bottom = 16.dp), + onClick = if (state.isLoading) { + null + } else { + { + state.items[index].route?.let { route -> + when (route) { + is DashboardPaginatedWidgetCardButtonRoute.HomeRoute -> { + homeNavController.navigate(route.route) + } + + is DashboardPaginatedWidgetCardButtonRoute.MainRoute -> { + mainNavController.navigate(route.route) + } + } + } + } + } + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + ) { + HorizonSpace(SpaceSize.SPACE_24) + + DashboardPaginatedWidgetCardItem( + item = state.items[index], + isLoading = state.isLoading, + modifier = Modifier + .padding(horizontal = 24.dp) + .semantics(mergeDescendants = true) { + role = Role.Button + } + ) + + HorizonSpace(SpaceSize.SPACE_24) + } + } + } + } +} + +@Composable +private fun DashboardPaginatedWidgetCardItem( + item: DashboardPaginatedWidgetCardItemState, + isLoading: Boolean, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxWidth() + ) { + if (item.chipState != null || item.pageState != null) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + item.chipState?.let { chipState -> + StatusChip( + state = StatusChipState( + label = chipState.label, + color = chipState.color, + fill = true + ), + modifier = Modifier.shimmerEffect( + isLoading, + backgroundColor = chipState.color.fillColor.copy(0.8f), + shimmerColor = chipState.color.fillColor.copy(0.5f) + ) + ) + } + + Spacer(Modifier.weight(1f)) + + item.pageState?.let { + Text( + it, + style = HorizonTypography.p2, + color = HorizonColors.Text.dataPoint(), + ) + } + } + HorizonSpace(SpaceSize.SPACE_16) + } + item.source?.let { source -> + Text( + text = stringResource( + R.string.dashboardAnnouncementBannerFrom, + source + ), + style = HorizonTypography.p2, + color = HorizonColors.Text.dataPoint(), + modifier = Modifier.shimmerEffect(isLoading) + ) + HorizonSpace(SpaceSize.SPACE_4) + } + item.date?.let { date -> + Text( + text = date.localisedFormat("MMM dd, yyyy"), + style = HorizonTypography.p2, + color = HorizonColors.Text.timestamp(), + modifier = Modifier.shimmerEffect(isLoading) + ) + HorizonSpace(SpaceSize.SPACE_8) + } + + item.title?.let { title -> + Text( + text = title, + style = HorizonTypography.p1, + color = HorizonColors.Text.body(), + modifier = Modifier + .fillMaxWidth() + .shimmerEffect(isLoading) + ) + + HorizonSpace(SpaceSize.SPACE_16) + } + } +} + +@Composable +@Preview +private fun DashboardPaginatedWidgetCardAnnouncementContentPreview() { + ContextKeeper.appContext = LocalContext.current + DashboardPaginatedWidgetCard( + state = DashboardPaginatedWidgetCardState( + items = listOf( + DashboardPaginatedWidgetCardItemState( + chipState = DashboardPaginatedWidgetCardChipState( + label = "Announcement", + color = StatusChipColor.Sky + ), + title = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Announcement title shown here.", + source = "Institution or Course Name Here", + date = Date(), + route = DashboardPaginatedWidgetCardButtonRoute.MainRoute("") + ), + DashboardPaginatedWidgetCardItemState( + chipState = DashboardPaginatedWidgetCardChipState( + label = "Announcement", + color = StatusChipColor.Sky + ), + title = "Second announcement with different content to show pagination.", + source = "Another Course Name", + date = Date(), + route = DashboardPaginatedWidgetCardButtonRoute.MainRoute("") + ), + DashboardPaginatedWidgetCardItemState( + chipState = DashboardPaginatedWidgetCardChipState( + label = "Announcement", + color = StatusChipColor.Sky + ), + title = "Third global announcement without a source.", + date = Date(), + route = DashboardPaginatedWidgetCardButtonRoute.MainRoute("") + ) + ) + ), + rememberNavController(), + rememberNavController(), + ) +} + +@Composable +@Preview +private fun DashboardPaginatedWidgetCardAnnouncementLoadingPreview() { + ContextKeeper.appContext = LocalContext.current + DashboardPaginatedWidgetCard( + state = DashboardPaginatedWidgetCardState( + items = listOf( + DashboardPaginatedWidgetCardItemState( + chipState = DashboardPaginatedWidgetCardChipState( + label = "Announcement", + color = StatusChipColor.Sky + ), + title = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Announcement title shown here.", + source = "Institution or Course Name Here", + date = Date(), + route = DashboardPaginatedWidgetCardButtonRoute.MainRoute("") + ), + ), + isLoading = true + ), + rememberNavController(), + rememberNavController(), + ) +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/DashboardPaginatedWidgetCardState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/DashboardPaginatedWidgetCardState.kt new file mode 100644 index 0000000000..f843a694a0 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/DashboardPaginatedWidgetCardState.kt @@ -0,0 +1,28 @@ +package com.instructure.horizon.features.dashboard.widget + +import com.instructure.horizon.horizonui.molecules.StatusChipColor +import java.util.Date + +data class DashboardPaginatedWidgetCardState( + val items: List = emptyList(), + val isLoading: Boolean = false, +) + +data class DashboardPaginatedWidgetCardItemState( + val chipState: DashboardPaginatedWidgetCardChipState? = null, + val pageState: String? = null, + val source: String? = null, + val date: Date? = null, + val title: String? = null, + val route: DashboardPaginatedWidgetCardButtonRoute? = null +) + +data class DashboardPaginatedWidgetCardChipState( + val label: String, + val color: StatusChipColor, +) + +sealed class DashboardPaginatedWidgetCardButtonRoute { + data class MainRoute(val route: String) : DashboardPaginatedWidgetCardButtonRoute() + data class HomeRoute(val route: String) : DashboardPaginatedWidgetCardButtonRoute() +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/DashboardWidgetCard.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/DashboardWidgetCard.kt new file mode 100644 index 0000000000..9895079e92 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/DashboardWidgetCard.kt @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardCard +import com.instructure.horizon.horizonui.animation.shimmerEffect +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonSpace +import com.instructure.horizon.horizonui.foundation.HorizonTypography +import com.instructure.horizon.horizonui.foundation.SpaceSize +import com.instructure.pandautils.compose.modifiers.conditional + +@Composable +fun DashboardWidgetCard( + title: String, + @DrawableRes iconRes: Int, + widgetColor: Color, + modifier: Modifier = Modifier, + isLoading: Boolean = false, + useMinWidth: Boolean = true, + onClick: (() -> Unit)? = null, + content: @Composable ColumnScope.() -> Unit +) { + val context = LocalContext.current + DashboardCard( + modifier + .semantics(mergeDescendants = true) { } + .conditional(isLoading) { + clearAndSetSemantics { + contentDescription = + context.getString(R.string.a11y_dashboardWidgetLoadingContentDescription, title) + } + }, + onClick + ) { + Column( + modifier = Modifier + .padding(24.dp) + .conditional(useMinWidth) { + width(IntrinsicSize.Min) + } + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = title, + style = HorizonTypography.labelMediumBold, + color = HorizonColors.Text.dataPoint(), + modifier = Modifier + .padding(end = 8.dp) + .shimmerEffect(isLoading) + ) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .clip(CircleShape) + .background(widgetColor) + .padding(6.dp) + .shimmerEffect( + isLoading, + backgroundColor = widgetColor.copy(alpha = 0.8f), + shimmerColor = widgetColor.copy(alpha = 0.5f) + ) + ) { + Icon( + painter = painterResource(iconRes), + contentDescription = null, + tint = HorizonColors.Icon.default(), + modifier = Modifier + .size(16.dp) + ) + } + } + + HorizonSpace(SpaceSize.SPACE_8) + + content() + } + } +} + +@Composable +@Preview +private fun DashboardTimeSpentCardPreview() { + DashboardWidgetCard( + title = "Time", + iconRes = R.drawable.schedule, + widgetColor = HorizonColors.PrimitivesBlue.blue12() + ) { + Text( + text = "Content", + style = HorizonTypography.h1, + color = HorizonColors.Text.body() + ) + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/DashboardWidgetCardError.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/DashboardWidgetCardError.kt new file mode 100644 index 0000000000..2253a612c7 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/DashboardWidgetCardError.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import com.instructure.horizon.R +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonSpace +import com.instructure.horizon.horizonui.foundation.HorizonTypography +import com.instructure.horizon.horizonui.foundation.SpaceSize +import com.instructure.horizon.horizonui.molecules.Button +import com.instructure.horizon.horizonui.molecules.ButtonColor +import com.instructure.horizon.horizonui.molecules.ButtonHeight +import com.instructure.horizon.horizonui.molecules.ButtonIconPosition + +@Composable +fun DashboardWidgetCardError( + title: String, + @DrawableRes iconRes: Int, + widgetColor: Color, + useMinWidth: Boolean, + onRetryClick: () -> Unit, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + DashboardWidgetCard( + title = title, + iconRes = iconRes, + widgetColor = widgetColor, + useMinWidth = useMinWidth, + modifier = modifier + .semantics(mergeDescendants = true) { + role = Role.Button + onClick(context.getString(R.string.dashboardWidgetCardErrorRetry)) { + onRetryClick() + true + } + } + ) { + Column( + modifier = Modifier.width(IntrinsicSize.Max) + ) { + Text( + text = stringResource(R.string.dashboardWidgetCardErrorMessage), + style = HorizonTypography.p2, + color = HorizonColors.Text.timestamp() + ) + HorizonSpace(SpaceSize.SPACE_8) + Button( + label = stringResource(R.string.dashboardWidgetCardErrorRetry), + onClick = onRetryClick, + color = ButtonColor.WhiteWithOutline, + height = ButtonHeight.SMALL, + iconPosition = ButtonIconPosition.End(R.drawable.restart_alt), + modifier = Modifier.clearAndSetSemantics { } + ) + } + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerRepository.kt new file mode 100644 index 0000000000..d7b2575462 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerRepository.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.announcement + +import com.instructure.canvasapi2.apis.AccountNotificationAPI +import com.instructure.canvasapi2.apis.AnnouncementAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.models.DiscussionTopicHeader.ReadState +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.depaginate +import com.instructure.horizon.features.inbox.HorizonInboxItemType +import com.instructure.horizon.features.inbox.navigation.HorizonInboxRoute +import javax.inject.Inject + +class DashboardAnnouncementBannerRepository @Inject constructor( + private val announcementApi: AnnouncementAPI.AnnouncementInterface, + private val accountNotificationApi: AccountNotificationAPI.AccountNotificationInterface, + private val getCoursesManager: HorizonGetCoursesManager, + private val apiPrefs: ApiPrefs, +) { + suspend fun getUnreadAnnouncements(forceRefresh: Boolean): List { + val courseAnnouncements = getUnreadCourseAnnouncements(forceRefresh) + val globalAnnouncements = getUnreadGlobalAnnouncements(forceRefresh) + + return (courseAnnouncements + globalAnnouncements) + .sortedByDescending { it.date } + } + + private suspend fun getUnreadCourseAnnouncements(forceRefresh: Boolean): List { + val params = RestParams(isForceReadFromNetwork = forceRefresh, usePerPageQueryParam = true) + val courses = getCourses(forceRefresh) + + if (courses.isEmpty()) { + return emptyList() + } + + return announcementApi.getFirstPageAnnouncements( + courseCode = courses.map { "course_" + it.courseId }.toTypedArray(), + params = params + ) + .depaginate { announcementApi.getNextPageAnnouncementsList(it, params) } + .dataOrThrow + .filter { it.status == ReadState.UNREAD } + .map { announcement -> + val course = courses.first { it.courseId == announcement.contextCode?.removePrefix("course_")?.toLong() } + AnnouncementBannerItem( + title = announcement.title.orEmpty(), + source = course.courseName, + date = announcement.postedDate, + type = AnnouncementType.COURSE, + route = + HorizonInboxRoute.InboxDetails.route( + type = HorizonInboxItemType.CourseNotification, + id = announcement.id, + courseId = announcement.contextCode?.removePrefix("course_")?.toLong() + ) + ) + } + } + + private suspend fun getUnreadGlobalAnnouncements(forceRefresh: Boolean): List { + val params = RestParams(isForceReadFromNetwork = forceRefresh, usePerPageQueryParam = true) + return accountNotificationApi.getAccountNotifications( + params, + includePast = true, + showIsClosed = true + ) + .depaginate { accountNotificationApi.getNextPageNotifications(it, params) } + .dataOrThrow + .filter { !it.closed } + .map { notification -> + AnnouncementBannerItem( + title = notification.subject, + date = notification.startDate, + type = AnnouncementType.GLOBAL, + route = + HorizonInboxRoute.InboxDetails.route( + type = HorizonInboxItemType.AccountNotification, + id = notification.id, + courseId = null + ) + ) + } + } + + suspend fun getCourses(forceNetwork: Boolean): List { + return getCoursesManager.getCoursesWithProgress(apiPrefs.user!!.id, forceNetwork).dataOrThrow + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerUiState.kt new file mode 100644 index 0000000000..330ce51ec3 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerUiState.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.announcement + +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardState +import java.util.Date + +data class DashboardAnnouncementBannerUiState( + val state: DashboardItemState = DashboardItemState.LOADING, + val cardState: DashboardPaginatedWidgetCardState = DashboardPaginatedWidgetCardState(), + val onRefresh: (() -> Unit) -> Unit, +) + +data class AnnouncementBannerItem( + val title: String, + val source: String? = null, + val date: Date?, + val type: AnnouncementType, + val route: String +) + +enum class AnnouncementType { + COURSE, + GLOBAL +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerViewModel.kt new file mode 100644 index 0000000000..5a66d060f1 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerViewModel.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.announcement + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.utils.weave.catch +import com.instructure.canvasapi2.utils.weave.tryLaunch +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardEvent +import com.instructure.horizon.features.dashboard.DashboardEventHandler +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardButtonRoute +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardChipState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardItemState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardState +import com.instructure.horizon.horizonui.molecules.StatusChipColor +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DashboardAnnouncementBannerViewModel @Inject constructor( + private val repository: DashboardAnnouncementBannerRepository, + private val dashboardEventHandler: DashboardEventHandler, + @ApplicationContext private val context: Context +) : ViewModel() { + + private val _uiState = MutableStateFlow( + DashboardAnnouncementBannerUiState( + onRefresh = ::refresh + ) + ) + val uiState = _uiState.asStateFlow() + + init { + viewModelScope.tryLaunch { + loadAnnouncementData() + } catch { + _uiState.update { it.copy(state = DashboardItemState.ERROR) } + } + + viewModelScope.launch { + dashboardEventHandler.events.collect { event -> + when (event) { + is DashboardEvent.AnnouncementRefresh -> { + refresh() + } + else -> { /* No-op */ } + } + } + } + } + + private suspend fun loadAnnouncementData(forceNetwork: Boolean = false) { + _uiState.update { it.copy(state = DashboardItemState.LOADING) } + val announcements = repository.getUnreadAnnouncements(forceNetwork) + + _uiState.update { + it.copy( + state = DashboardItemState.SUCCESS, + cardState = DashboardPaginatedWidgetCardState( + items = announcements.mapIndexed { index, announcement -> + announcement.toPaginatedWidgetCardItemState(context, index, announcements.size) + } + ) + ) + } + } + + private fun refresh(onComplete: () -> Unit = {}) { + viewModelScope.tryLaunch { + _uiState.update { it.copy(state = DashboardItemState.LOADING) } + loadAnnouncementData(forceNetwork = true) + _uiState.update { it.copy(state = DashboardItemState.SUCCESS) } + onComplete() + } catch { + _uiState.update { it.copy(state = DashboardItemState.ERROR) } + onComplete() + } + } +} + +private fun AnnouncementBannerItem.toPaginatedWidgetCardItemState( + context: Context, + itemIndex: Int, + size: Int, +): DashboardPaginatedWidgetCardItemState { + return DashboardPaginatedWidgetCardItemState( + chipState = DashboardPaginatedWidgetCardChipState( + label = context.getString(R.string.notificationsAnnouncementCategoryLabel), + color = StatusChipColor.Sky + ), + pageState = if (size > 1) { + context.getString(R.string.dsahboardPaginatedWidgetPagerMessage, itemIndex + 1, size) + } else { + null + }, + source = source, + date = date, + title = title, + route = DashboardPaginatedWidgetCardButtonRoute.MainRoute(route) + ) +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerWidget.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerWidget.kt new file mode 100644 index 0000000000..d949af71e9 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerWidget.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.announcement + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCard +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardButtonRoute +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardChipState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardItemState +import com.instructure.horizon.features.dashboard.widget.announcement.card.DashboardAnnouncementBannerCardError +import com.instructure.horizon.horizonui.molecules.StatusChipColor +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import java.util.Date + +@Composable +fun DashboardAnnouncementBannerWidget( + mainNavController: NavHostController, + homeNavController: NavHostController, + shouldRefresh: Boolean, + refreshState: MutableStateFlow> +) { + val viewModel = hiltViewModel() + val state by viewModel.uiState.collectAsState() + + LaunchedEffect(shouldRefresh) { + if (shouldRefresh) { + refreshState.update { it + true } + state.onRefresh { + refreshState.update { it - true } + } + } + } + + if (state.state != DashboardItemState.SUCCESS || state.cardState.items.isNotEmpty()) { + DashboardAnnouncementBannerSection(state, mainNavController, homeNavController) + } +} + +@Composable +fun DashboardAnnouncementBannerSection( + state: DashboardAnnouncementBannerUiState, + mainNavController: NavHostController, + homeNavController: NavHostController, +) { + when (state.state) { + DashboardItemState.LOADING -> { + DashboardPaginatedWidgetCard( + state.cardState.copy( + items = listOf( + DashboardPaginatedWidgetCardItemState( + chipState = DashboardPaginatedWidgetCardChipState( + label = stringResource(R.string.notificationsAnnouncementCategoryLabel), + color = StatusChipColor.Sky + ), + title = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Announcement title shown here.", + source = "Institution or Course Name Here", + date = Date(), + route = DashboardPaginatedWidgetCardButtonRoute.MainRoute("") + ) + ), + isLoading = true + ), + mainNavController, + homeNavController, + ) + } + DashboardItemState.ERROR -> { + DashboardAnnouncementBannerCardError( + { state.onRefresh {} }, + Modifier.padding(horizontal = 16.dp) + ) + } + DashboardItemState.SUCCESS -> { + DashboardPaginatedWidgetCard( + state.cardState, + mainNavController, + homeNavController, + ) + } + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/announcement/card/DashboardAnnouncementBannerCardError.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/announcement/card/DashboardAnnouncementBannerCardError.kt new file mode 100644 index 0000000000..0e0db7701b --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/announcement/card/DashboardAnnouncementBannerCardError.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.announcement.card + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardCard +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonSpace +import com.instructure.horizon.horizonui.foundation.HorizonTypography +import com.instructure.horizon.horizonui.foundation.SpaceSize +import com.instructure.horizon.horizonui.molecules.Button +import com.instructure.horizon.horizonui.molecules.ButtonColor +import com.instructure.horizon.horizonui.molecules.ButtonHeight +import com.instructure.horizon.horizonui.molecules.ButtonIconPosition +import com.instructure.horizon.horizonui.molecules.StatusChip +import com.instructure.horizon.horizonui.molecules.StatusChipColor +import com.instructure.horizon.horizonui.molecules.StatusChipState + +@Composable +fun DashboardAnnouncementBannerCardError( + onRetry: () -> Unit, + modifier: Modifier = Modifier +) { + DashboardCard( + modifier = modifier + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp) + ) { + StatusChip( + state = StatusChipState( + label = stringResource(R.string.notificationsAnnouncementCategoryLabel), + color = StatusChipColor.Sky, + fill = true + ), + ) + HorizonSpace(SpaceSize.SPACE_16) + Text( + text = stringResource(R.string.dashboardAnnouncementBannerErrorMessage), + style = HorizonTypography.p2, + color = HorizonColors.Text.timestamp() + ) + HorizonSpace(SpaceSize.SPACE_8) + Button( + label = stringResource(R.string.dashboardAnnouncementRefreshMessage), + iconPosition = ButtonIconPosition.End(R.drawable.restart_alt), + height = ButtonHeight.SMALL, + onClick = onRetry, + color = ButtonColor.BlackOutline + ) + } + } +} + +@Composable +@Preview +private fun DashboardAnnouncementBannerCardErrorPreview() { + ContextKeeper.appContext = LocalContext.current + DashboardAnnouncementBannerCardError(onRetry = {}) +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/DashboardCourseRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/DashboardCourseRepository.kt new file mode 100644 index 0000000000..fa2bae79c2 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/DashboardCourseRepository.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.course + +import com.instructure.canvasapi2.GetCoursesQuery +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetProgramsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Program +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.utils.ApiPrefs +import javax.inject.Inject + +class DashboardCourseRepository @Inject constructor( + private val horizonGetCoursesManager: HorizonGetCoursesManager, + private val moduleApi: ModuleAPI.ModuleInterface, + private val apiPrefs: ApiPrefs, + private val enrollmentApi: EnrollmentAPI.EnrollmentInterface, + private val getProgramsManager: GetProgramsManager, +) { + suspend fun getEnrollments(forceNetwork: Boolean): List { + return horizonGetCoursesManager.getEnrollments(apiPrefs.user?.id ?: -1, forceNetwork).dataOrThrow + } + + suspend fun acceptInvite(courseId: Long, enrollmentId: Long) { + return enrollmentApi.acceptInvite(courseId, enrollmentId, RestParams()).dataOrThrow + } + + suspend fun getPrograms(forceNetwork: Boolean = false): List { + return getProgramsManager.getPrograms(forceNetwork) + } + + suspend fun getFirstPageModulesWithItems(courseId: Long, forceNetwork: Boolean): List { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return moduleApi.getFirstPageModulesWithItems( + CanvasContext.Type.COURSE.apiString, + courseId, + params, + includes = listOf("estimated_durations") + ).dataOrThrow + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/DashboardCourseSection.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/DashboardCourseSection.kt new file mode 100644 index 0000000000..baafacd090 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/DashboardCourseSection.kt @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.course + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardCard +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCard +import com.instructure.horizon.features.dashboard.widget.course.card.CardClickAction +import com.instructure.horizon.features.dashboard.widget.course.card.DashboardCourseCardContent +import com.instructure.horizon.features.dashboard.widget.course.card.DashboardCourseCardError +import com.instructure.horizon.features.dashboard.widget.course.card.DashboardCourseCardLoading +import com.instructure.horizon.features.dashboard.widget.course.card.DashboardCourseCardState +import com.instructure.horizon.features.home.HomeNavigationRoute +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonTypography +import com.instructure.horizon.horizonui.organisms.AnimatedHorizontalPager +import com.instructure.horizon.horizonui.organisms.AnimatedHorizontalPagerIndicator +import com.instructure.horizon.navigation.MainNavigationRoute +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +@Composable +fun DashboardCourseSection( + mainNavController: NavHostController, + homeNavController: NavHostController, + shouldRefresh: Boolean, + refreshState: MutableStateFlow> +) { + val viewModel = hiltViewModel() + val state by viewModel.uiState.collectAsState() + + LaunchedEffect(shouldRefresh) { + if (shouldRefresh) { + refreshState.update { it + true } + state.onRefresh { + refreshState.update { it - true } + } + } + } + + DashboardCourseSection(state, mainNavController, homeNavController) +} + +@Composable +fun DashboardCourseSection( + state: DashboardCourseUiState, + mainNavController: NavHostController, + homeNavController: NavHostController +) { + when(state.state) { + DashboardItemState.LOADING -> { + DashboardCourseCardLoading(Modifier.padding(horizontal = 24.dp)) + } + DashboardItemState.ERROR -> { + DashboardCourseCardError({state.onRefresh {} }, Modifier.padding(horizontal = 24.dp)) + } + DashboardItemState.SUCCESS -> { + DashboardCourseSectionContent(state, mainNavController, homeNavController) + } + } +} + +@Composable +private fun DashboardCourseSectionContent( + state: DashboardCourseUiState, + mainNavController: NavHostController, + homeNavController: NavHostController +) { + val pagerState = rememberPagerState { state.courses.size } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (state.programs.items.isNotEmpty()) { + DashboardPaginatedWidgetCard( + state.programs, + mainNavController, + homeNavController, + ) + } + + if (state.courses.isNotEmpty()) { + AnimatedHorizontalPager( + pagerState, + beyondViewportPageCount = pagerState.pageCount, + contentPadding = PaddingValues(horizontal = 24.dp), + pageSpacing = 12.dp, + verticalAlignment = Alignment.CenterVertically, + ) { index, modifier -> + DashboardCourseItem( + state.courses[index], + mainNavController, + homeNavController, + modifier.padding(bottom = 8.dp) + ) + } + + if (pagerState.pageCount >= 4) { + AnimatedHorizontalPagerIndicator(pagerState) + } + } else { + DashboardCard( + Modifier.padding(horizontal = 24.dp) + ) { + Text( + stringResource(R.string.dashboardNoCoursesMessage), + style = HorizonTypography.h4, + color = HorizonColors.Text.body(), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 24.dp) + ) + } + } + } +} + +@Composable +private fun DashboardCourseItem( + cardState: DashboardCourseCardState, + mainNavController: NavHostController, + homeNavController: NavHostController, + modifier: Modifier = Modifier +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier.fillMaxWidth() + ){ + DashboardCourseCardContent( + cardState, { handleClickAction(it, mainNavController, homeNavController) } + ) + } +} + +private fun handleClickAction( + action: CardClickAction?, + mainNavController: NavHostController, + homeNavController: NavHostController +) { + when(action) { + is CardClickAction.Action -> { + action.onClick() + } + is CardClickAction.NavigateToCourse -> { + homeNavController.navigate(HomeNavigationRoute.Learn.withCourse(action.courseId)) { + popUpTo(homeNavController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = false + } + } + is CardClickAction.NavigateToModuleItem -> { + mainNavController.navigate( + MainNavigationRoute.ModuleItemSequence( + action.courseId, + action.moduleItemId + ) + ) + } + is CardClickAction.NavigateToProgram -> { + homeNavController.navigate(HomeNavigationRoute.Learn.withProgram(action.programId)) { + popUpTo(homeNavController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = false + } + } + else -> {} + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/DashboardCourseUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/DashboardCourseUiState.kt new file mode 100644 index 0000000000..893baf0dac --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/DashboardCourseUiState.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.course + +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.course.card.DashboardCourseCardState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardState + +data class DashboardCourseUiState( + val state: DashboardItemState = DashboardItemState.LOADING, + val programs: DashboardPaginatedWidgetCardState = DashboardPaginatedWidgetCardState(), + val courses: List = emptyList(), + val onRefresh: (onFinished: () -> Unit)-> Unit = { } +) \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/DashboardCourseViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/DashboardCourseViewModel.kt new file mode 100644 index 0000000000..bbcc31c2c3 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/DashboardCourseViewModel.kt @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.course + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.type.EnrollmentWorkflowState +import com.instructure.canvasapi2.utils.weave.catch +import com.instructure.canvasapi2.utils.weave.tryLaunch +import com.instructure.horizon.features.dashboard.DashboardEvent +import com.instructure.horizon.features.dashboard.DashboardEventHandler +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.course.card.CardClickAction +import com.instructure.horizon.features.dashboard.widget.course.card.DashboardCourseCardModuleItemState +import com.instructure.horizon.features.dashboard.widget.course.card.DashboardCourseCardState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardState +import com.instructure.horizon.model.LearningObjectType +import com.instructure.journey.type.ProgramProgressCourseEnrollmentStatus +import com.instructure.pandautils.utils.formatIsoDuration +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DashboardCourseViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val repository: DashboardCourseRepository, + private val dashboardEventHandler: DashboardEventHandler +): ViewModel() { + private val _uiState = MutableStateFlow(DashboardCourseUiState(onRefresh = ::onRefresh)) + val uiState = _uiState.asStateFlow() + + init { + loadData() + + viewModelScope.launch { + dashboardEventHandler.events.collect { event -> + when (event) { + is DashboardEvent.ProgressRefresh -> { + onRefresh() + } + else -> { /* No-op */ } + } + } + } + } + + private fun loadData() { + _uiState.update { it.copy(state = DashboardItemState.LOADING) } + + viewModelScope.tryLaunch { + fetchData(forceNetwork = false) + _uiState.update { it.copy(state = DashboardItemState.SUCCESS) } + } catch { + _uiState.update { it.copy(state = DashboardItemState.ERROR) } + } + } + + private fun onRefresh(onFinished: () -> Unit = {}) { + viewModelScope.tryLaunch { + _uiState.update { it.copy(state = DashboardItemState.LOADING) } + fetchData(forceNetwork = true) + _uiState.update { it.copy(state = DashboardItemState.SUCCESS) } + onFinished() + } catch { + _uiState.update { it.copy(state = DashboardItemState.ERROR) } + onFinished() + } + } + + private suspend fun fetchData(forceNetwork: Boolean) { + var enrollments = repository.getEnrollments(forceNetwork) + val programs = repository.getPrograms(forceNetwork) + val invitations = enrollments.filter { it.state == EnrollmentWorkflowState.invited } + + // Accept invitations automatically + if (invitations.isNotEmpty()) { + invitations.forEach { enrollment -> + repository.acceptInvite( + enrollment.course?.id?.toLongOrNull() ?: return@forEach, + enrollment.id?.toLongOrNull() ?: return@forEach + ) + } + enrollments = repository.getEnrollments(true) + } + + + val courseCardStates = enrollments.mapToDashboardCourseCardState( + context, + programs = programs, + nextModuleForCourse = { courseId -> + fetchNextModuleState(courseId, forceNetwork) + }, + ).map { state -> + if (state.buttonState?.onClickAction is CardClickAction.Action) { + state.copy(buttonState = state.buttonState.copy( + onClickAction = CardClickAction.Action { + viewModelScope.tryLaunch { + updateCourseButtonState(state, isLoading = true) + state.buttonState.action() + onRefresh() + updateCourseButtonState(state, isLoading = false) + } catch { + updateCourseButtonState(state, isLoading = false) + } + }, + )) + } else state + } + + val programCardStates = programs + .filter { program -> program.sortedRequirements.none { it.enrollmentStatus == ProgramProgressCourseEnrollmentStatus.ENROLLED } } + .mapToDashboardCourseCardState(context) + + _uiState.update { + it.copy( + programs = DashboardPaginatedWidgetCardState(programCardStates), + courses = courseCardStates + ) + } + } + + private suspend fun fetchNextModuleState(courseId: Long?, forceNetwork: Boolean): DashboardCourseCardModuleItemState? { + if (courseId == null) return null + val modules = repository.getFirstPageModulesWithItems(courseId, forceNetwork = forceNetwork) + val nextModuleItem = modules.flatMap { module -> module.items }.firstOrNull() + val nextModule = modules.find { module -> module.id == nextModuleItem?.moduleId } + if (nextModuleItem == null) { + return null + } + + return DashboardCourseCardModuleItemState( + moduleItemTitle = nextModuleItem.title.orEmpty(), + moduleItemType = if (nextModuleItem.quizLti) LearningObjectType.ASSESSMENT else LearningObjectType.fromApiString(nextModuleItem.type.orEmpty()), + dueDate = nextModuleItem.moduleDetails?.dueDate, + estimatedDuration = nextModuleItem.estimatedDuration?.formatIsoDuration(context), + onClickAction = CardClickAction.NavigateToModuleItem(courseId, nextModuleItem.id) + ) + } + + private fun updateCourseButtonState(state: DashboardCourseCardState, isLoading: Boolean) { + _uiState.update { + it.copy( + courses = it.courses.map { originalState -> + if (originalState.title == state.title && originalState.parentPrograms == state.parentPrograms) { + originalState.copy( + buttonState = originalState.buttonState?.copy( + isLoading = isLoading + ) + ) + } else { + originalState + } + } + ) + } + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/DashboardMapper.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/DashboardMapper.kt new file mode 100644 index 0000000000..2eb876877c --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/DashboardMapper.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.course + +import android.content.Context +import com.instructure.canvasapi2.GetCoursesQuery +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Program +import com.instructure.canvasapi2.type.EnrollmentWorkflowState +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardButtonRoute +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardChipState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardItemState +import com.instructure.horizon.features.dashboard.widget.course.card.CardClickAction +import com.instructure.horizon.features.dashboard.widget.course.card.DashboardCourseCardImageState +import com.instructure.horizon.features.dashboard.widget.course.card.DashboardCourseCardModuleItemState +import com.instructure.horizon.features.dashboard.widget.course.card.DashboardCourseCardParentProgramState +import com.instructure.horizon.features.dashboard.widget.course.card.DashboardCourseCardState +import com.instructure.horizon.features.home.HomeNavigationRoute +import com.instructure.horizon.horizonui.molecules.StatusChipColor + +internal suspend fun List.mapToDashboardCourseCardState( + context: Context, + programs: List, + nextModuleForCourse: suspend (Long?) -> DashboardCourseCardModuleItemState? +): List { + val completed = this.filter { it.isCompleted() }.map { it.mapCompleted(context, programs) } + val active = this.filter { it.isActive() }.map { it.mapActive(programs, nextModuleForCourse) } + return (active + completed).sortedByDescending { course -> + course.progress.run { if (this == 100.0) -1.0 else this } // Active courses first, then completed courses + ?: 0.0 + } +} + +internal fun List.mapToDashboardCourseCardState(context: Context): List { + return this.mapIndexed { itemIndex, program -> + DashboardPaginatedWidgetCardItemState( + chipState = DashboardPaginatedWidgetCardChipState( + label = context.getString(R.string.dashboardCourseCardProgramChipLabel), + color = StatusChipColor.Grey + ), + pageState = if (size > 1) { + context.getString(R.string.dsahboardPaginatedWidgetPagerMessage, itemIndex + 1, size) + } else { + null + }, + title = context.getString( + R.string.dashboardCourseCardProgramDetailsMessage, + program.name + ), + route = DashboardPaginatedWidgetCardButtonRoute.HomeRoute(HomeNavigationRoute.Learn.withProgram(program.id)), + ) + } +} + +private fun GetCoursesQuery.Enrollment.isCompleted(): Boolean { + return this.state == EnrollmentWorkflowState.completed || this.isMaxProgress() +} + +private fun GetCoursesQuery.Enrollment.isActive(): Boolean { + return this.state == EnrollmentWorkflowState.active && !this.isMaxProgress() +} + +private fun GetCoursesQuery.Enrollment.isMaxProgress(): Boolean { + return this.course?.usersConnection?.nodes?.firstOrNull()?.courseProgression?.requirements?.completionPercentage == 100.0 +} + +private fun GetCoursesQuery.Enrollment.mapCompleted(context: Context, programs: List): DashboardCourseCardState { + return DashboardCourseCardState( + parentPrograms = programs + .filter { it.sortedRequirements.any { it.courseId == this.course?.id?.toLongOrNull() } } + .map { program -> + DashboardCourseCardParentProgramState( + programName = program.name, + programId = program.id, + onClickAction = CardClickAction.NavigateToProgram(program.id) + ) + }, + imageState = DashboardCourseCardImageState( + imageUrl = this.course?.image_download_url, + showPlaceholder = true + ), + title = this.course?.name.orEmpty(), + description = context.getString(R.string.dashboardCompletedCourseDetails), + progress = this.course?.usersConnection?.nodes?.firstOrNull()?.courseProgression?.requirements?.completionPercentage ?: 0.0, + moduleItem = null, + buttonState = null, + onClickAction = CardClickAction.NavigateToCourse(this.course?.id?.toLongOrNull() ?: -1L) + ) +} + +private suspend fun GetCoursesQuery.Enrollment.mapActive( + programs: List, + nextModuleForCourse: suspend (Long?) -> DashboardCourseCardModuleItemState? +): DashboardCourseCardState { + return DashboardCourseCardState( + parentPrograms = programs + .filter { it.sortedRequirements.any { it.courseId == this.course?.id?.toLongOrNull() } } + .map { program -> + DashboardCourseCardParentProgramState( + programName = program.name, + programId = program.id, + onClickAction = CardClickAction.NavigateToProgram(program.id) + ) + }, + imageState = DashboardCourseCardImageState( + imageUrl = this.course?.image_download_url, + showPlaceholder = true + ), + title = this.course?.name.orEmpty(), + description = null, + progress = this.course?.usersConnection?.nodes?.firstOrNull()?.courseProgression?.requirements?.completionPercentage ?: 0.0, + moduleItem = nextModuleForCourse(this.course?.id?.toLongOrNull()), + buttonState = null, + onClickAction = CardClickAction.NavigateToCourse(this.course?.id?.toLongOrNull() ?: -1L), + lastAccessed = this.lastActivityAt + ) +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/card/DashboardCourseCardContent.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/card/DashboardCourseCardContent.kt new file mode 100644 index 0000000000..85cb418b6f --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/card/DashboardCourseCardContent.kt @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.course.card + +import android.graphics.drawable.Drawable +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withLink +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardCard +import com.instructure.horizon.horizonui.animation.shimmerEffect +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius +import com.instructure.horizon.horizonui.foundation.HorizonSpace +import com.instructure.horizon.horizonui.foundation.HorizonTypography +import com.instructure.horizon.horizonui.foundation.SpaceSize +import com.instructure.horizon.horizonui.molecules.ButtonColor +import com.instructure.horizon.horizonui.molecules.ButtonHeight +import com.instructure.horizon.horizonui.molecules.ButtonIconPosition +import com.instructure.horizon.horizonui.molecules.ButtonWidth +import com.instructure.horizon.horizonui.molecules.LoadingButton +import com.instructure.horizon.horizonui.molecules.Pill +import com.instructure.horizon.horizonui.molecules.PillCase +import com.instructure.horizon.horizonui.molecules.PillSize +import com.instructure.horizon.horizonui.molecules.PillStyle +import com.instructure.horizon.horizonui.molecules.PillType +import com.instructure.horizon.horizonui.molecules.ProgressBarSmall +import com.instructure.horizon.horizonui.molecules.ProgressBarStyle +import com.instructure.horizon.horizonui.molecules.StatusChip +import com.instructure.horizon.horizonui.molecules.StatusChipState +import com.instructure.horizon.model.LearningObjectType +import com.instructure.pandautils.utils.localisedFormatMonthDay +import java.util.Date +import kotlin.math.roundToInt + +@Composable +fun DashboardCourseCardContent( + state: DashboardCourseCardState, + handleOnClickAction: (CardClickAction?) -> Unit, + modifier: Modifier = Modifier +) { + DashboardCard(modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = state.onClickAction != null) { + handleOnClickAction(state.onClickAction) + } + ) { + if (state.imageState != null) { + CourseImage(state.imageState) + } + Column( + modifier = Modifier + .padding(horizontal = 24.dp) + ) { + if (state.chipState != null) { + Spacer(Modifier.height(24.dp)) + CardChip(state.chipState) + } + if (!state.parentPrograms.isNullOrEmpty()) { + Spacer(Modifier.height(16.dp)) + ProgramsText(state.parentPrograms, handleOnClickAction) + } + if (!state.title.isNullOrEmpty()) { + Spacer(Modifier.height(16.dp)) + TitleText(state.title) + } + if (state.progress != null) { + Spacer(Modifier.height(12.dp)) + CourseProgress(state.progress) + } + if (!state.description.isNullOrEmpty()) { + Spacer(Modifier.height(16.dp)) + DescriptionText(state.description) + } + if (state.moduleItem != null) { + Spacer(Modifier.height(16.dp)) + ModuleItemCard(state.moduleItem, handleOnClickAction) + } + if (state.buttonState != null) { + Spacer(Modifier.height(16.dp)) + DashboardCardButton(state.buttonState, handleOnClickAction) + } + } + Spacer(Modifier.height(24.dp)) + } + } +} + +@OptIn(ExperimentalGlideComposeApi::class) +@Composable +private fun CourseImage(state: DashboardCourseCardImageState) { + var isLoading by rememberSaveable { mutableStateOf(true) } + if (!state.imageUrl.isNullOrEmpty()) { + GlideImage( + state.imageUrl, + contentDescription = null, + contentScale = ContentScale.FillBounds, + requestBuilderTransform = { + it.addListener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { + isLoading = false + return false + } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + isLoading = false + return false + } + + }) + }, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1.69f) + .shimmerEffect(isLoading) + ) + } else { + if (state.showPlaceholder) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1.69f) + .background(HorizonColors.Surface.institution().copy(alpha = 0.1f)) + ) { + Icon( + painterResource(R.drawable.book_2_filled), + contentDescription = null, + tint = HorizonColors.Surface.institution(), + modifier = Modifier.size(20.dp) + ) + } + } + } +} + +@Composable +private fun CardChip(state: DashboardCourseCardChipState) { + StatusChip( + StatusChipState( + label = state.label, + color = state.color, + fill = true, + iconRes = null + ) + ) +} + +@Composable +private fun ProgramsText( + programs: List, + handleOnClickAction: (CardClickAction?) -> Unit, +) { + val programsAnnotated = buildAnnotatedString { + programs.forEachIndexed { i, program -> + if (i > 0) append(", ") + withLink( + LinkAnnotation.Clickable( + tag = program.programId, + styles = TextLinkStyles( + style = SpanStyle(textDecoration = TextDecoration.Underline) + ), + linkInteractionListener = { _ -> handleOnClickAction(program.onClickAction) } + ) + ) { + append(program.programName) + } + } + } + + // String resource can't work with annotated string so we need a temporary placeholder + val template = stringResource(R.string.learnScreen_partOfProgram, "__PROGRAMS__") + + val fullText = buildAnnotatedString { + val parts = template.split("__PROGRAMS__") + append(parts[0]) + append(programsAnnotated) + if (parts.size > 1) append(parts[1]) + } + + Text( + modifier = Modifier.semantics(mergeDescendants = true) {}, + style = HorizonTypography.p1, + text = fullText + ) +} + +@Composable +private fun TitleText(title: String) { + Text( + text = title, + style = HorizonTypography.h4, + color = HorizonColors.Text.title(), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) +} + +@Composable +private fun DescriptionText(description: String) { + Text( + text = description, + style = HorizonTypography.p1, + color = HorizonColors.Text.body(), + ) +} + +@Composable +private fun CourseProgress(progress: Double) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + ProgressBarSmall( + progress = progress, + style = ProgressBarStyle.Institution, + showLabels = false, + modifier = Modifier.weight(1f) + ) + + HorizonSpace(SpaceSize.SPACE_8) + + Text( + text = stringResource(R.string.progressBar_percent, progress.roundToInt()), + style = HorizonTypography.p2, + color = HorizonColors.Surface.institution(), + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun ModuleItemCard( + state: DashboardCourseCardModuleItemState, + handleOnClickAction: (CardClickAction?) -> Unit, +) { + Box( + modifier = Modifier + .fillMaxWidth() + .background( + color = HorizonColors.Surface + .institution() + .copy(alpha = 0.1f), + shape = HorizonCornerRadius.level2 + ) + .clip(HorizonCornerRadius.level2) + .clickable { handleOnClickAction(state.onClickAction) } + .padding(16.dp) + ) { + Column { + Text( + text = state.moduleItemTitle, + style = HorizonTypography.p1, + color = HorizonColors.Text.body() + ) + Spacer(Modifier.height(12.dp)) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Pill( + label = stringResource(state.moduleItemType.stringRes), + size = PillSize.SMALL, + style = PillStyle.SOLID, + type = PillType.INVERSE, + case = PillCase.TITLE, + iconRes = state.moduleItemType.iconRes, + ) + + if (state.dueDate != null) { + Pill( + label = stringResource(R.string.learningobject_dueDate, state.dueDate.localisedFormatMonthDay()), + size = PillSize.SMALL, + style = PillStyle.SOLID, + type = PillType.INVERSE, + case = PillCase.TITLE, + iconRes = R.drawable.calendar_today, + ) + } + + if (state.estimatedDuration != null) { + Pill( + label = state.estimatedDuration, + size = PillSize.SMALL, + style = PillStyle.SOLID, + type = PillType.INVERSE, + case = PillCase.TITLE, + iconRes = R.drawable.calendar_today, + ) + } + } + } + } +} + +@Composable +private fun DashboardCardButton( + state: DashboardCourseCardButtonState, + handleOnClickAction: (CardClickAction?) -> Unit +) { + LoadingButton( + label = state.label, + height = ButtonHeight.SMALL, + width = ButtonWidth.RELATIVE, + color = ButtonColor.Black, + iconPosition = ButtonIconPosition.NoIcon, + onClick = { handleOnClickAction(state.onClickAction) }, + contentAlignment = Alignment.Center, + loading = state.isLoading, + ) +} + +@Composable +@Preview +private fun DashboardCourseCardWithModulePreview() { + ContextKeeper.appContext = LocalContext.current + + val state = DashboardCourseCardState( + parentPrograms = listOf( + DashboardCourseCardParentProgramState( + programName = "Program Name", + programId = "1", + onClickAction = CardClickAction.Action({}) + ) + ), + imageState = null, + title = "Course Title That Might Be Really Long and Go On Two Lines", + description = "This is a description of the course. It might be really long and go on multiple lines.", + progress = 45.0, + moduleItem = DashboardCourseCardModuleItemState( + moduleItemTitle = "Module Item Title That Might Be Really Long and Go On Two Lines", + moduleItemType = LearningObjectType.ASSIGNMENT, + dueDate = Date(), + estimatedDuration = "5 mins", + onClickAction = CardClickAction.Action({}) + ), + buttonState = DashboardCourseCardButtonState( + label = "Go to Course", + onClickAction = CardClickAction.Action({}) + ), + onClickAction = CardClickAction.Action({}) + ) + DashboardCourseCardContent(state, {}) +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/card/DashboardCourseCardError.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/card/DashboardCourseCardError.kt new file mode 100644 index 0000000000..2ef8f8dd5c --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/card/DashboardCourseCardError.kt @@ -0,0 +1,75 @@ +package com.instructure.horizon.features.dashboard.widget.course.card + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardCard +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonTypography +import com.instructure.horizon.horizonui.molecules.Button +import com.instructure.horizon.horizonui.molecules.ButtonColor +import com.instructure.horizon.horizonui.molecules.ButtonHeight +import com.instructure.horizon.horizonui.molecules.ButtonIconPosition +import com.instructure.horizon.horizonui.molecules.ButtonWidth + +@Composable +fun DashboardCourseCardError( + onRetry: () -> Unit, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + DashboardCard( + modifier + .semantics(mergeDescendants = true) { + role = Role.Button + onClick(context.getString(R.string.dashboardCourseCardRefreshLabel)) { + onRetry() + true + } + contentDescription = + context.getString(R.string.a11y_dashboardCoursesSectionTitle) + } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 40.dp, horizontal = 24.dp) + ) { + Text( + text = stringResource(R.string.dashboardCourseCardErrorTitle), + style = HorizonTypography.h4, + color = HorizonColors.Text.title() + ) + Text( + text = stringResource(R.string.dashboardCourseCardErrorMessage), + style = HorizonTypography.p2, + color = HorizonColors.Text.timestamp() + ) + Spacer(modifier = Modifier.height(16.dp)) + Button( + label = stringResource(R.string.dashboardCourseCardRefreshLabel), + height = ButtonHeight.SMALL, + width = ButtonWidth.RELATIVE, + color = ButtonColor.WhiteWithOutline, + iconPosition = ButtonIconPosition.End(R.drawable.restart_alt), + onClick = onRetry, + modifier = Modifier.clearAndSetSemantics { } + ) + } + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/card/DashboardCourseCardLoading.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/card/DashboardCourseCardLoading.kt new file mode 100644 index 0000000000..f13dd1fa93 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/card/DashboardCourseCardLoading.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.course.card + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardCard +import com.instructure.horizon.horizonui.animation.shimmerEffect +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius +import com.instructure.horizon.horizonui.foundation.HorizonSpace +import com.instructure.horizon.horizonui.foundation.SpaceSize + +@Composable +fun DashboardCourseCardLoading( + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + DashboardCard( + modifier + .clearAndSetSemantics { + contentDescription = context.getString( + R.string.a11y_dashboardWidgetLoadingContentDescription, + context.getString(R.string.a11y_dashboardCoursesSectionTitle) + ) + }, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding() + ) { + Box( + Modifier + .fillMaxWidth() + .aspectRatio(1.69f) + .shimmerEffect( + true, + shape = HorizonCornerRadius.level0, + ) + ) + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + ) { + HorizonSpace(SpaceSize.SPACE_8) + + Box( + Modifier + .fillMaxWidth() + .height(50.dp) + .shimmerEffect(true) + ) + + HorizonSpace(SpaceSize.SPACE_8) + + Box( + Modifier + .fillMaxWidth() + .height(25.dp) + .shimmerEffect( + true, + ) + ) + + HorizonSpace(SpaceSize.SPACE_8) + + Box( + Modifier + .fillMaxWidth() + .height(25.dp) + .shimmerEffect( + true, + shape = HorizonCornerRadius.level6, + backgroundColor = HorizonColors.Surface.institution().copy(alpha = 0.1f) + ) + ) + + HorizonSpace(SpaceSize.SPACE_8) + + Box( + Modifier + .fillMaxWidth() + .height(100.dp) + .shimmerEffect( + true, + shape = HorizonCornerRadius.level2, + backgroundColor = HorizonColors.Surface.institution().copy(alpha = 0.1f) + ) + ) + + HorizonSpace(SpaceSize.SPACE_24) + } + } + } +} + +@Composable +@Preview +private fun DashboardCourseCardLoadingPreview() { + DashboardCourseCardLoading() +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/card/DashboardCourseCardState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/card/DashboardCourseCardState.kt new file mode 100644 index 0000000000..75e83a51f4 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/course/card/DashboardCourseCardState.kt @@ -0,0 +1,56 @@ +package com.instructure.horizon.features.dashboard.widget.course.card + +import com.instructure.horizon.horizonui.molecules.StatusChipColor +import com.instructure.horizon.model.LearningObjectType +import java.util.Date + +data class DashboardCourseCardState( + val chipState: DashboardCourseCardChipState? = null, + val parentPrograms: List? = null, + val imageState: DashboardCourseCardImageState? = null, + val title: String? = null, + val description: String? = null, + val progress: Double? = null, + val moduleItem: DashboardCourseCardModuleItemState? = null, + val buttonState: DashboardCourseCardButtonState? = null, + val onClickAction: CardClickAction? = null, + val lastAccessed: Date? = null, +) + +data class DashboardCourseCardParentProgramState( + val programName: String, + val programId: String, + val onClickAction: CardClickAction, +) + +data class DashboardCourseCardModuleItemState( + val moduleItemTitle: String, + val moduleItemType: LearningObjectType, + val dueDate: Date? = null, + val estimatedDuration: String? = null, + val onClickAction: CardClickAction, +) + +data class DashboardCourseCardButtonState( + val label: String, + val onClickAction: CardClickAction, + val isLoading: Boolean = false, + val action: suspend () -> Unit = { }, +) + +data class DashboardCourseCardChipState( + val label: String, + val color: StatusChipColor, +) + +data class DashboardCourseCardImageState( + val imageUrl: String? = null, + val showPlaceholder: Boolean = false, +) + +sealed class CardClickAction { + data class NavigateToProgram(val programId: String): CardClickAction() + data class NavigateToCourse(val courseId: Long): CardClickAction() + data class NavigateToModuleItem(val courseId: Long, val moduleItemId: Long): CardClickAction() + data class Action(val onClick: () -> Unit): CardClickAction() +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressRepository.kt new file mode 100644 index 0000000000..d1572c676c --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressRepository.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.myprogress + +import com.google.gson.annotations.SerializedName +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetWidgetsManager +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.horizon.util.deserializeDynamicObject +import java.util.Date +import javax.inject.Inject + +data class MyProgressWidgetData( + val data: List, + val lastModifiedDate: Date?, +) + +data class MyProgressWidgetDataEntry( + @SerializedName("course_id") + val courseId: Long?, + + @SerializedName("course_name") + val courseName: String?, + + @SerializedName("user_id") + val userId: Long?, + + @SerializedName("user_uuid") + val userUUID: String?, + + @SerializedName("user_name") + val userName: String?, + + @SerializedName("user_avatar_image_url") + val userAvatarUrl: String?, + + @SerializedName("user_email") + val userEmail: String?, + + @SerializedName("module_count_completed") + val moduleCountCompleted: Int?, + + @SerializedName("module_count_started") + val moduleCountStarted: Int?, + + @SerializedName("module_count_locked") + val moduleCountLocked: Int?, + + @SerializedName("module_count_total") + val moduleCountTotal: Int?, +) + +class DashboardMyProgressRepository @Inject constructor( + private val apiPrefs: ApiPrefs, + private val getWidgetsManager: GetWidgetsManager, + private val getCoursesManager: HorizonGetCoursesManager, +) { + suspend fun getLearningStatusData(courseId: Long? = null, forceNetwork: Boolean): MyProgressWidgetData? { + return getWidgetsManager.getLearningStatusWidgetData(courseId, forceNetwork).deserializeDynamicObject() + } + + suspend fun getCourses(forceNetwork: Boolean): List { + return getCoursesManager.getCoursesWithProgress(apiPrefs.user?.id ?: 0, forceNetwork).dataOrThrow + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressUiState.kt new file mode 100644 index 0000000000..dc51291f99 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressUiState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.myprogress + +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.myprogress.card.DashboardMyProgressCardState + +data class DashboardMyProgressUiState( + val state: DashboardItemState = DashboardItemState.LOADING, + val cardState: DashboardMyProgressCardState = DashboardMyProgressCardState(), + val onRefresh: (onComplete: () -> Unit) -> Unit = {} +) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressViewModel.kt new file mode 100644 index 0000000000..0458e9dc89 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressViewModel.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.myprogress + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.utils.weave.catch +import com.instructure.canvasapi2.utils.weave.tryLaunch +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.myprogress.card.DashboardMyProgressCardState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class DashboardMyProgressViewModel @Inject constructor( + private val repository: DashboardMyProgressRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow( + DashboardMyProgressUiState( + onRefresh = ::refresh + ) + ) + val uiState = _uiState.asStateFlow() + + init { + loadMyProgressData() + } + + private fun loadMyProgressData(forceNetwork: Boolean = false) { + viewModelScope.tryLaunch { + _uiState.update { it.copy(state = DashboardItemState.LOADING) } + val courses = repository.getCourses(forceNetwork) + val data = repository.getLearningStatusData(forceNetwork = forceNetwork)?.data + .orEmpty() + .filter { dataEntry -> courses.any { it.courseId == dataEntry.courseId } } + val moduleCountCompleted = data.sumOf { it.moduleCountCompleted ?: 0} + + _uiState.update { + it.copy( + state = DashboardItemState.SUCCESS, + cardState = DashboardMyProgressCardState( + moduleCountCompleted = moduleCountCompleted + ) + ) + } + } catch { + _uiState.update { it.copy(state = DashboardItemState.ERROR) } + } + } + + private fun refresh(onComplete: () -> Unit) { + viewModelScope.tryLaunch { + _uiState.update { it.copy(state = DashboardItemState.LOADING) } + loadMyProgressData(forceNetwork = true) + _uiState.update { it.copy(state = DashboardItemState.SUCCESS) } + onComplete() + } catch { + _uiState.update { it.copy(state = DashboardItemState.ERROR) } + onComplete() + } + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressWidget.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressWidget.kt new file mode 100644 index 0000000000..6ac669b2e5 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressWidget.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.myprogress + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.DashboardWidgetCardError +import com.instructure.horizon.features.dashboard.widget.myprogress.card.DashboardMyProgressCardContent +import com.instructure.horizon.horizonui.foundation.HorizonColors +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +@Composable +fun DashboardMyProgressWidget( + shouldRefresh: Boolean, + refreshState: MutableStateFlow>, + modifier: Modifier = Modifier +) { + val viewModel = hiltViewModel() + val state by viewModel.uiState.collectAsState() + + LaunchedEffect(shouldRefresh) { + if (shouldRefresh) { + refreshState.update { it + true } + state.onRefresh { + refreshState.update { it - true } + } + } + } + + DashboardMyProgressSection(state, modifier) +} + +@Composable +fun DashboardMyProgressSection( + state: DashboardMyProgressUiState, + modifier: Modifier = Modifier +) { + when (state.state) { + DashboardItemState.LOADING -> { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth() + ) { + DashboardMyProgressCardContent( + state.cardState, + true, + modifier + ) + } + } + DashboardItemState.ERROR -> { + DashboardWidgetCardError( + stringResource(R.string.dashboardMyProgressTitle), + R.drawable.trending_up, + HorizonColors.PrimitivesSky.sky12, + false, + { state.onRefresh {} }, + modifier = modifier + ) + } + DashboardItemState.SUCCESS -> { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth(), + ) { + DashboardMyProgressCardContent( + state.cardState, + false, + modifier + ) + } + } + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/card/DashboardMyProgressCardContent.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/card/DashboardMyProgressCardContent.kt new file mode 100644 index 0000000000..750752613d --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/card/DashboardMyProgressCardContent.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.myprogress.card + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.widget.DashboardWidgetCard +import com.instructure.horizon.horizonui.animation.shimmerEffect +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonTypography + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun DashboardMyProgressCardContent( + state: DashboardMyProgressCardState, + isLoading: Boolean, + modifier: Modifier = Modifier, +) { + DashboardWidgetCard( + stringResource(R.string.dashboardMyProgressTitle), + R.drawable.trending_up, + HorizonColors.PrimitivesSky.sky12, + useMinWidth = false, + isLoading = isLoading, + modifier = modifier + ) { + if(state.moduleCountCompleted == 0) { + Text( + text = stringResource(R.string.dashboardMyProgressEmptyMessage), + style = HorizonTypography.p2, + color = HorizonColors.Text.timestamp(), + modifier = Modifier + .width(IntrinsicSize.Max) + .shimmerEffect(isLoading) + ) + } else { + val context = LocalContext.current + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .clearAndSetSemantics { + contentDescription = context.getString( + R.string.a11y_dashboardMyProgressContentDescription, + state.moduleCountCompleted + ) + } + ) { + Text( + text = state.moduleCountCompleted.toString(), + style = HorizonTypography.h1.copy(fontSize = 38.sp, letterSpacing = 0.sp), + color = HorizonColors.Text.body(), + modifier = Modifier.shimmerEffect(isLoading) + ) + + Text( + text = stringResource(R.string.dashboardMyProgressCompleted), + style = HorizonTypography.labelMediumBold, + color = HorizonColors.Text.title(), + modifier = Modifier + .align(Alignment.CenterVertically) + .shimmerEffect(isLoading) + ) + } + } + } +} + +@Composable +@Preview +private fun DashboardMyProgressCardContentPreview() { + DashboardMyProgressCardContent( + state = DashboardMyProgressCardState( + moduleCountCompleted = 24 + ), + false + ) +} + +@Composable +@Preview +private fun DashboardMyProgressCardContentZeroPreview() { + DashboardMyProgressCardContent( + state = DashboardMyProgressCardState( + moduleCountCompleted = 0 + ), + false + ) +} + +@Composable +@Preview +private fun DashboardMyProgressLoadingPreview() { + DashboardMyProgressCardContent( + state = DashboardMyProgressCardState( + moduleCountCompleted = 0 + ), + isLoading = true + ) +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/card/DashboardMyProgressCardState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/card/DashboardMyProgressCardState.kt new file mode 100644 index 0000000000..892724666b --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/myprogress/card/DashboardMyProgressCardState.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.myprogress.card + +data class DashboardMyProgressCardState( + val moduleCountCompleted: Int = 0 +) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsRepository.kt new file mode 100644 index 0000000000..7af3211741 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsRepository.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skillhighlights + +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetSkillsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Skill +import javax.inject.Inject + +class DashboardSkillHighlightsRepository @Inject constructor( + private val getSkillsManager: GetSkillsManager +) { + suspend fun getSkills(completedOnly: Boolean?, forceNetwork: Boolean): List { + return getSkillsManager.getSkills(completedOnly = completedOnly, forceNetwork = forceNetwork) + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsUiState.kt new file mode 100644 index 0000000000..e5e08d0e3e --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsUiState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skillhighlights + +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.skillhighlights.card.DashboardSkillHighlightsCardState + +data class DashboardSkillHighlightsUiState( + val state: DashboardItemState = DashboardItemState.LOADING, + val cardState: DashboardSkillHighlightsCardState = DashboardSkillHighlightsCardState(), + val onRefresh: (() -> Unit) -> Unit = {} +) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsViewModel.kt new file mode 100644 index 0000000000..d7d43ac40a --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsViewModel.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skillhighlights + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.utils.weave.catch +import com.instructure.canvasapi2.utils.weave.tryLaunch +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.skillhighlights.card.DashboardSkillHighlightsCardState +import com.instructure.horizon.features.dashboard.widget.skillhighlights.card.SkillHighlight +import com.instructure.horizon.features.dashboard.widget.skillhighlights.card.SkillHighlightProficiencyLevel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class DashboardSkillHighlightsViewModel @Inject constructor( + private val repository: DashboardSkillHighlightsRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow( + DashboardSkillHighlightsUiState( + onRefresh = ::refresh + ) + ) + val uiState = _uiState.asStateFlow() + + init { + viewModelScope.tryLaunch{ + loadSkillsData() + } catch { + _uiState.update { it.copy(state = DashboardItemState.ERROR) } + } + } + + private suspend fun loadSkillsData(forceNetwork: Boolean = false) { + _uiState.update { it.copy(state = DashboardItemState.LOADING) } + val skills = repository.getSkills(completedOnly = true, forceNetwork = forceNetwork) + + val topSkills = skills + .map { skill -> + SkillHighlight( + name = skill.name, + proficiencyLevel = SkillHighlightProficiencyLevel.fromString(skill.proficiencyLevel) + ?: SkillHighlightProficiencyLevel.BEGINNER + ) + } + .sortedWith( + compareByDescending { it.proficiencyLevel.levelOrder } + .thenBy { it.name } + ) + .take(3) + + if (topSkills.size < 3) { + _uiState.update { it.copy(state = DashboardItemState.SUCCESS) } + } else { + _uiState.update { + it.copy( + state = DashboardItemState.SUCCESS, + cardState = DashboardSkillHighlightsCardState( + skills = topSkills + ) + ) + } + } + } + + private fun refresh(onComplete: () -> Unit) { + viewModelScope.tryLaunch { + _uiState.update { it.copy(state = DashboardItemState.LOADING) } + loadSkillsData(forceNetwork = true) + _uiState.update { it.copy(state = DashboardItemState.SUCCESS) } + onComplete() + } catch { + _uiState.update { it.copy(state = DashboardItemState.ERROR) } + onComplete() + } + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsWidget.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsWidget.kt new file mode 100644 index 0000000000..f4ba223bee --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsWidget.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skillhighlights + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.DashboardWidgetCardError +import com.instructure.horizon.features.dashboard.widget.skillhighlights.card.DashboardSkillHighlightsCardContent +import com.instructure.horizon.horizonui.foundation.HorizonColors +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +@Composable +fun DashboardSkillHighlightsWidget( + homeNavController: NavHostController, + shouldRefresh: Boolean, + refreshState: MutableStateFlow>, + modifier: Modifier = Modifier +) { + val viewModel = hiltViewModel() + val state by viewModel.uiState.collectAsState() + + LaunchedEffect(shouldRefresh) { + if (shouldRefresh) { + refreshState.update { it + true } + state.onRefresh { + refreshState.update { it - true } + } + } + } + + DashboardSkillHighlightsSection(state, homeNavController, modifier) +} + +@Composable +fun DashboardSkillHighlightsSection( + state: DashboardSkillHighlightsUiState, + homeNavController: NavHostController, + modifier: Modifier = Modifier +) { + when (state.state) { + DashboardItemState.LOADING -> { + DashboardSkillHighlightsCardContent( + state.cardState, + homeNavController, + true, + modifier.padding(horizontal = 24.dp), + ) + } + DashboardItemState.ERROR -> { + DashboardWidgetCardError( + stringResource(R.string.dashboardSkillHighlightsTitle), + R.drawable.hub, + HorizonColors.PrimitivesGreen.green12(), + false, + { state.onRefresh {} }, + modifier = modifier.padding(horizontal = 24.dp) + ) + } + DashboardItemState.SUCCESS -> { + DashboardSkillHighlightsCardContent( + state.cardState, + homeNavController, + false, + modifier.padding(horizontal = 24.dp), + ) + } + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/card/DashboardSkillHighlightsCardContent.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/card/DashboardSkillHighlightsCardContent.kt new file mode 100644 index 0000000000..1a74cf7c34 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/card/DashboardSkillHighlightsCardContent.kt @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skillhighlights.card + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.widget.DashboardWidgetCard +import com.instructure.horizon.features.home.HomeNavigationRoute +import com.instructure.horizon.horizonui.animation.shimmerEffect +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonSpace +import com.instructure.horizon.horizonui.foundation.HorizonTypography +import com.instructure.horizon.horizonui.foundation.SpaceSize +import com.instructure.horizon.horizonui.molecules.Pill +import com.instructure.horizon.horizonui.molecules.PillCase +import com.instructure.horizon.horizonui.molecules.PillSize +import com.instructure.horizon.horizonui.molecules.PillStyle +import com.instructure.horizon.horizonui.molecules.PillType + +@Composable +fun DashboardSkillHighlightsCardContent( + state: DashboardSkillHighlightsCardState, + homeNavController: NavHostController, + isLoading: Boolean, + modifier: Modifier = Modifier, +) { + DashboardWidgetCard( + title = stringResource(R.string.dashboardSkillHighlightsTitle), + iconRes = R.drawable.hub, + widgetColor = HorizonColors.PrimitivesGreen.green12(), + isLoading = isLoading, + useMinWidth = false, + modifier = modifier + ) { + if (state.skills.isEmpty()) { + Column { + HorizonSpace(SpaceSize.SPACE_8) + Text( + text = stringResource(R.string.dashboardSkillHighlightsNoDataTitle), + style = HorizonTypography.h4, + color = HorizonColors.Text.title(), + modifier = Modifier.shimmerEffect(isLoading) + ) + HorizonSpace(SpaceSize.SPACE_4) + Text( + text = stringResource(R.string.dashboardSkillHighlightsNoDataMessage), + style = HorizonTypography.p2, + color = HorizonColors.Text.timestamp(), + modifier = Modifier.shimmerEffect(isLoading) + ) + } + } else { + HorizonSpace(SpaceSize.SPACE_8) + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + state.skills.forEach { skill -> + SkillCard( + skill, + skill.proficiencyLevel.opacity(), + homeNavController, + modifier = Modifier.shimmerEffect( + isLoading, + backgroundColor = HorizonColors.PrimitivesGreen.green12().copy(alpha = 0.8f), + shimmerColor = HorizonColors.PrimitivesGreen.green12().copy(alpha = 0.5f) + ) + ) + } + } + } + } +} + +@Composable +private fun SkillCard( + skill: SkillHighlight, + opacity: Float, + homeNavController: NavHostController, + modifier: Modifier = Modifier +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .clip(RoundedCornerShape(16.dp)) + .background(HorizonColors.PrimitivesGreen.green12().copy(alpha = opacity)) + .fillMaxWidth() + .clickable { + homeNavController.navigate(HomeNavigationRoute.Skillspace.route) { + popUpTo(homeNavController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + .padding(24.dp) + ) { + Text( + text = skill.name, + style = HorizonTypography.p2, + color = HorizonColors.Text.body(), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Pill( + label = stringResource(skill.proficiencyLevel.skillProficiencyLevelRes), + style = PillStyle.SOLID, + type = PillType.INVERSE, + case = PillCase.TITLE, + size = PillSize.SMALL + ) + } +} + +private fun SkillHighlightProficiencyLevel.opacity(): Float { + return when (this) { + SkillHighlightProficiencyLevel.BEGINNER -> 0.4f + SkillHighlightProficiencyLevel.PROFICIENT -> 0.6f + SkillHighlightProficiencyLevel.ADVANCED -> 0.8f + SkillHighlightProficiencyLevel.EXPERT -> 1f + } +} + +@Composable +@Preview +private fun DashboardSkillHighlightsCardContentPreview() { + ContextKeeper.appContext = LocalContext.current + DashboardSkillHighlightsCardContent( + state = DashboardSkillHighlightsCardState( + skills = listOf( + SkillHighlight("Dolor sit amet adipiscing elit do long skill name", SkillHighlightProficiencyLevel.ADVANCED), + SkillHighlight("Dolor sit skill name", SkillHighlightProficiencyLevel.BEGINNER), + SkillHighlight("Adipiscing elit skill name", SkillHighlightProficiencyLevel.PROFICIENT) + ) + ), + rememberNavController(), + false + ) +} + +@Composable +@Preview +private fun DashboardSkillHighlightsCardContentNoDataPreview() { + ContextKeeper.appContext = LocalContext.current + DashboardSkillHighlightsCardContent( + state = DashboardSkillHighlightsCardState(skills = emptyList()), + rememberNavController(), + false + ) +} + +@Composable +@Preview +private fun DashboardSkillHighlightsLoadingPreview() { + ContextKeeper.appContext = LocalContext.current + DashboardSkillHighlightsCardContent( + state = DashboardSkillHighlightsCardState(skills = emptyList()), + rememberNavController(), + isLoading = true + ) +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/card/DashboardSkillHighlightsCardState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/card/DashboardSkillHighlightsCardState.kt new file mode 100644 index 0000000000..be8da4680e --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/card/DashboardSkillHighlightsCardState.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skillhighlights.card + +import androidx.annotation.StringRes +import com.instructure.horizon.R + +data class DashboardSkillHighlightsCardState( + val skills: List = emptyList() +) + +data class SkillHighlight( + val name: String, + val proficiencyLevel: SkillHighlightProficiencyLevel +) + +enum class SkillHighlightProficiencyLevel( + @StringRes val skillProficiencyLevelRes: Int, + val apiString: String, + val levelOrder: Int +) { + BEGINNER(R.string.dashboardSkillProficienyLevelBeginner, "beginner", 0), + PROFICIENT(R.string.dashboardSkillProficienyLevelProficient, "proficient", 1), + ADVANCED(R.string.dashboardSkillProficienyLevelAdvanced, "advanced", 2), + EXPERT(R.string.dashboardSkillProficienyLevelExpert, "expert", 3); + + companion object { + fun fromString(level: String?): SkillHighlightProficiencyLevel? { + return SkillHighlightProficiencyLevel.entries.firstOrNull { it.apiString.equals(level, ignoreCase = true) } + } + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewRepository.kt new file mode 100644 index 0000000000..843334ef1e --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewRepository.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skilloverview + +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetSkillsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Skill +import javax.inject.Inject + +class DashboardSkillOverviewRepository @Inject constructor( + private val getSkillsManager: GetSkillsManager +) { + suspend fun getSkills(completedOnly: Boolean?, forceNetwork: Boolean): List { + return getSkillsManager.getSkills(completedOnly = completedOnly, forceNetwork = forceNetwork) + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewUiState.kt new file mode 100644 index 0000000000..5cd5bf3701 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewUiState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skilloverview + +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.skilloverview.card.DashboardSkillOverviewCardState + +data class DashboardSkillOverviewUiState( + val state: DashboardItemState = DashboardItemState.LOADING, + val cardState: DashboardSkillOverviewCardState = DashboardSkillOverviewCardState(), + val onRefresh: (() -> Unit) -> Unit = {} +) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewViewModel.kt new file mode 100644 index 0000000000..990c16e632 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewViewModel.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skilloverview + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.utils.weave.catch +import com.instructure.canvasapi2.utils.weave.tryLaunch +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.skilloverview.card.DashboardSkillOverviewCardState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class DashboardSkillOverviewViewModel @Inject constructor( + private val repository: DashboardSkillOverviewRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow( + DashboardSkillOverviewUiState( + onRefresh = ::refresh + ) + ) + val uiState = _uiState.asStateFlow() + + init { + viewModelScope.tryLaunch { + loadSkillOverviewData() + } catch { + _uiState.update { it.copy(state = DashboardItemState.ERROR) } + } + } + + private suspend fun loadSkillOverviewData(forceNetwork: Boolean = false) { + _uiState.update { it.copy(state = DashboardItemState.LOADING) } + val skills = repository.getSkills(completedOnly = true, forceNetwork = forceNetwork) + + val completedSkillCount = skills.size + + _uiState.update { + it.copy( + state = DashboardItemState.SUCCESS, + cardState = DashboardSkillOverviewCardState( + completedSkillCount = completedSkillCount + ) + ) + } + } + + private fun refresh(onComplete: () -> Unit) { + viewModelScope.tryLaunch { + _uiState.update { it.copy(state = DashboardItemState.LOADING) } + loadSkillOverviewData(forceNetwork = true) + _uiState.update { it.copy(state = DashboardItemState.SUCCESS) } + onComplete() + } catch { + _uiState.update { it.copy(state = DashboardItemState.ERROR) } + onComplete() + } + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewWidget.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewWidget.kt new file mode 100644 index 0000000000..8b46c1d732 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewWidget.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skilloverview + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.DashboardWidgetCardError +import com.instructure.horizon.features.dashboard.widget.skilloverview.card.DashboardSkillOverviewCardContent +import com.instructure.horizon.horizonui.foundation.HorizonColors +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +@Composable +fun DashboardSkillOverviewWidget( + homeNavController: NavHostController, + shouldRefresh: Boolean, + refreshState: MutableStateFlow>, + modifier: Modifier = Modifier +) { + val viewModel = hiltViewModel() + val state by viewModel.uiState.collectAsState() + + LaunchedEffect(shouldRefresh) { + if (shouldRefresh) { + refreshState.update { it + true } + state.onRefresh { + refreshState.update { it - true } + } + } + } + + DashboardSkillOverviewSection(state, homeNavController, modifier) +} + +@Composable +fun DashboardSkillOverviewSection( + state: DashboardSkillOverviewUiState, + homeNavController: NavHostController, + modifier: Modifier = Modifier +) { + when (state.state) { + DashboardItemState.LOADING -> { + DashboardSkillOverviewCardContent( + state.cardState, + homeNavController, + true, + modifier + ) + } + DashboardItemState.ERROR -> { + DashboardWidgetCardError( + stringResource(R.string.dashboardSkillOverviewTitle), + R.drawable.hub, + HorizonColors.PrimitivesGreen.green12(), + false, + { state.onRefresh {} }, + modifier = modifier + ) + } + DashboardItemState.SUCCESS -> { + DashboardSkillOverviewCardContent( + state.cardState, + homeNavController, + false, + modifier + ) + } + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/card/DashboardSkillOverviewCardContent.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/card/DashboardSkillOverviewCardContent.kt new file mode 100644 index 0000000000..77567167d4 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/card/DashboardSkillOverviewCardContent.kt @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skilloverview.card + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.widget.DashboardWidgetCard +import com.instructure.horizon.features.home.HomeNavigationRoute +import com.instructure.horizon.horizonui.animation.shimmerEffect +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonTypography + +@Composable +fun DashboardSkillOverviewCardContent( + state: DashboardSkillOverviewCardState, + homeNavController: NavHostController, + isLoading: Boolean, + modifier: Modifier = Modifier, +) { + DashboardWidgetCard( + title = stringResource(R.string.dashboardSkillOverviewTitle), + iconRes = R.drawable.hub, + widgetColor = HorizonColors.PrimitivesGreen.green12(), + isLoading = isLoading, + useMinWidth = false, + onClick = { + homeNavController.navigate(HomeNavigationRoute.Skillspace.route) { + popUpTo(homeNavController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + modifier = modifier + ) { + if (state.completedSkillCount == 0) { + Text( + text = stringResource(R.string.dashboardSkillOverviewNoDataMessage), + style = HorizonTypography.p2, + color = HorizonColors.Text.timestamp(), + modifier = Modifier + .width(IntrinsicSize.Max) + .shimmerEffect(isLoading) + ) + } else { + val context = LocalContext.current + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clearAndSetSemantics { + contentDescription = context.getString( + R.string.a11y_dashboardSkillOverviewContentDescription, + state.completedSkillCount + ) + } + ) { + Text( + text = state.completedSkillCount.toString(), + style = HorizonTypography.h1.copy(fontSize = 38.sp, letterSpacing = 0.sp), + color = HorizonColors.Text.body(), + modifier = Modifier.shimmerEffect(isLoading) + ) + Text( + text = stringResource(R.string.dashboardSkillOverviewEarnedLabel), + style = HorizonTypography.labelMediumBold, + color = HorizonColors.Text.title(), + modifier = Modifier.shimmerEffect(isLoading) + ) + } + } + } +} + +@Composable +@Preview +private fun DashboardSkillOverviewCardContentPreview() { + ContextKeeper.appContext = LocalContext.current + DashboardSkillOverviewCardContent( + state = DashboardSkillOverviewCardState(completedSkillCount = 24), + rememberNavController(), + false + ) +} + +@Composable +@Preview +private fun DashboardSkillOverviewCardContentNoDataPreview() { + ContextKeeper.appContext = LocalContext.current + DashboardSkillOverviewCardContent( + state = DashboardSkillOverviewCardState(completedSkillCount = 0), + rememberNavController(), + false + ) +} + +@Composable +@Preview +private fun DashboardSkillOverviewLoadingPreview() { + ContextKeeper.appContext = LocalContext.current + DashboardSkillOverviewCardContent( + state = DashboardSkillOverviewCardState(completedSkillCount = 0), + rememberNavController(), + isLoading = true + ) +} + diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/card/DashboardSkillOverviewCardState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/card/DashboardSkillOverviewCardState.kt new file mode 100644 index 0000000000..8fabe83d8b --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/skilloverview/card/DashboardSkillOverviewCardState.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skilloverview.card + +data class DashboardSkillOverviewCardState( + val completedSkillCount: Int = 0 +) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentRepository.kt new file mode 100644 index 0000000000..46245c78bc --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentRepository.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.timespent + +import com.google.gson.annotations.SerializedName +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetWidgetsManager +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.horizon.util.deserializeDynamicList +import java.util.Date +import javax.inject.Inject + +data class TimeSpentWidgetData( + val lastModifiedDate: Date?, + val data: List +) + +data class TimeSpentDataEntry( + val date: Date? = null, + + @SerializedName("user_id") + val userId: Long? = null, + + @SerializedName("user_uuid") + val userUUID: String? = null, + + @SerializedName("user_name") + val userName: String? = null, + + @SerializedName("user_email") + val userEmail: String? = null, + + @SerializedName("user_avatar_image_url") + val userAvatarUrl: String? = null, + + @SerializedName("course_id") + val courseId: Long? = null, + + @SerializedName("course_name") + val courseName: String? = null, + + @SerializedName("minutes_per_day") + val minutesPerDay: Int? = null, +) + +class DashboardTimeSpentRepository @Inject constructor( + private val getWidgetsManager: GetWidgetsManager, + private val getCoursesManager: HorizonGetCoursesManager, + private val apiPrefs: ApiPrefs, +) { + suspend fun getTimeSpentData(courseId: Long? = null, forceNetwork: Boolean): TimeSpentWidgetData { + val widgetData = getWidgetsManager.getTimeSpentWidgetData(courseId, forceNetwork) + return TimeSpentWidgetData( + lastModifiedDate = widgetData.lastModifiedDate, + data = widgetData.data.deserializeDynamicList() + ) + } + + suspend fun getCourses(forceNetwork: Boolean): List { + return getCoursesManager.getCoursesWithProgress(apiPrefs.user!!.id, forceNetwork).dataOrThrow + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentUiState.kt new file mode 100644 index 0000000000..55de06e0fe --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentUiState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.timespent + +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.timespent.card.DashboardTimeSpentCardState + +data class DashboardTimeSpentUiState( + val state: DashboardItemState = DashboardItemState.LOADING, + val cardState: DashboardTimeSpentCardState = DashboardTimeSpentCardState(), + val onRefresh: (onComplete: () -> Unit) -> Unit = {} +) \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentViewModel.kt new file mode 100644 index 0000000000..1ef90c0772 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentViewModel.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.timespent + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.utils.weave.catch +import com.instructure.canvasapi2.utils.weave.tryLaunch +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.timespent.card.CourseOption +import com.instructure.horizon.features.dashboard.widget.timespent.card.DashboardTimeSpentCardState +import com.instructure.pandautils.utils.orDefault +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class DashboardTimeSpentViewModel @Inject constructor( + private val repository: DashboardTimeSpentRepository +) : ViewModel() { + + private var courses: List = emptyList() + private var timeSpentData: TimeSpentWidgetData? = null + + private val _uiState = MutableStateFlow( + DashboardTimeSpentUiState( + onRefresh = ::refresh + ) + ) + val uiState = _uiState.asStateFlow() + + init { + loadTimeSpentData() + } + + private fun loadTimeSpentData(forceNetwork: Boolean = false) { + viewModelScope.tryLaunch { + _uiState.update { it.copy(state = DashboardItemState.LOADING) } + courses = repository.getCourses(forceNetwork).map { + CourseOption( + id = it.courseId, + name = it.courseName, + ) + } + + timeSpentData = repository.getTimeSpentData(null, forceNetwork) + updateTimeSpentWidgetState() + + } catch { + _uiState.update { it.copy(state = DashboardItemState.ERROR) } + } + } + + private fun updateTimeSpentWidgetState() { + val totalMinutes = timeSpentData?.data + ?.filter { courses.any { course -> course.id == it.courseId } } + ?.filter { uiState.value.cardState.selectedCourseId == null || it.courseId == uiState.value.cardState.selectedCourseId } + ?.sumOf { it.minutesPerDay?.toDouble() ?: 0.0 } + .orDefault() + val hours = (totalMinutes / 60).toInt() + val minutes = (totalMinutes % 60).toInt() + + _uiState.update { + it.copy( + state = DashboardItemState.SUCCESS, + cardState = DashboardTimeSpentCardState( + hours = hours, + minutes = minutes, + courses = courses, + selectedCourseId = it.cardState.selectedCourseId, + onCourseSelected = ::onCourseSelected + ) + ) + } + } + + private fun onCourseSelected(courseName: String?) { + val courseId = uiState.value.cardState.courses.firstOrNull { it.name == courseName }?.id + _uiState.update { + it.copy( + cardState = it.cardState.copy(selectedCourseId = courseId) + ) + } + updateTimeSpentWidgetState() + } + + private fun refresh(onComplete: () -> Unit = {}) { + viewModelScope.tryLaunch { + _uiState.update { it.copy(state = DashboardItemState.LOADING) } + loadTimeSpentData(forceNetwork = true) + _uiState.update { it.copy(state = DashboardItemState.SUCCESS) } + onComplete() + } catch { + _uiState.update { it.copy(state = DashboardItemState.ERROR) } + onComplete() + } + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentWidget.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentWidget.kt new file mode 100644 index 0000000000..7d15c923fe --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentWidget.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.timespent + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.DashboardWidgetCardError +import com.instructure.horizon.features.dashboard.widget.timespent.card.DashboardTimeSpentCardContent +import com.instructure.horizon.horizonui.foundation.HorizonColors +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +@Composable +fun DashboardTimeSpentWidget( + shouldRefresh: Boolean, + refreshState: MutableStateFlow>, + modifier: Modifier = Modifier +) { + val viewModel = hiltViewModel() + val state by viewModel.uiState.collectAsState() + + LaunchedEffect(shouldRefresh) { + if (shouldRefresh) { + refreshState.update { it + true } + state.onRefresh { + refreshState.update { it - true } + } + } + } + + DashboardTimeSpentSection(state, modifier) +} + +@Composable +fun DashboardTimeSpentSection( + state: DashboardTimeSpentUiState, + modifier: Modifier = Modifier +) { + when (state.state) { + DashboardItemState.LOADING -> { + DashboardTimeSpentCardContent(state.cardState, true, modifier) + } + DashboardItemState.ERROR -> { + DashboardWidgetCardError( + stringResource(R.string.dashboardTimeSpentTitle), + R.drawable.schedule, + HorizonColors.PrimitivesHoney.honey12(), + false, + { state.onRefresh {} }, + modifier = modifier + ) + } + DashboardItemState.SUCCESS -> { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth() + ) { + DashboardTimeSpentCardContent(state.cardState, false, modifier) + } + } + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/card/DashboardTimeSpentCardContent.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/card/DashboardTimeSpentCardContent.kt new file mode 100644 index 0000000000..41c2e4a935 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/card/DashboardTimeSpentCardContent.kt @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.timespent.card + +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.widget.DashboardWidgetCard +import com.instructure.horizon.horizonui.animation.shimmerEffect +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonSpace +import com.instructure.horizon.horizonui.foundation.HorizonTypography +import com.instructure.horizon.horizonui.foundation.SpaceSize +import com.instructure.horizon.horizonui.organisms.inputs.singleselect.SingleSelect +import com.instructure.horizon.horizonui.organisms.inputs.singleselect.SingleSelectInputSize +import com.instructure.horizon.horizonui.organisms.inputs.singleselect.SingleSelectState + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun DashboardTimeSpentCardContent( + state: DashboardTimeSpentCardState, + isLoading: Boolean, + modifier: Modifier = Modifier, +) { + DashboardWidgetCard( + stringResource(R.string.dashboardTimeSpentTitle), + R.drawable.schedule, + HorizonColors.PrimitivesHoney.honey12(), + modifier, + isLoading, + false + ) { + if (state.hours == 0 && state.minutes == 0 && state.courses.isEmpty()) { + Text( + text = stringResource(R.string.dashboardTimeSpentEmptyMessage), + style = HorizonTypography.p2, + color = HorizonColors.Text.timestamp(), + modifier = Modifier + .width(IntrinsicSize.Max) + .shimmerEffect(isLoading) + ) + } else { + val courseValue = state.courses.firstOrNull { it.id == state.selectedCourseId }?.name + ?: stringResource(R.string.dashboardTimeSpentTotal) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + ) { + if (state.minutes == 0) { + DashboardTimeSpentSingleTimeUnit( + state.hours, + pluralStringResource(R.plurals.dashboardTimeSpentHoursUnit, state.hours), + courseValue, + isLoading + ) + } else if (state.hours == 0) { + DashboardTimeSpentSingleTimeUnit( + state.minutes, + pluralStringResource(R.plurals.dashboardTimeSpentMinutesUnit, state.minutes), + courseValue, + isLoading + ) + } else { + DashboardTimeSpentMultiTimeUnit( + hours = state.hours, + minutes = state.minutes, + courseValue = courseValue, + isLoading = isLoading + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .align(Alignment.CenterVertically) + .fillMaxWidth() + ) { + if (state.courses.size > 1) { + HorizonSpace(SpaceSize.SPACE_8) + + var isMenuOpen by remember { mutableStateOf(false) } + val courseSelectState = SingleSelectState( + isMenuOpen = isMenuOpen, + onMenuOpenChanged = { isMenuOpen = it }, + size = SingleSelectInputSize.Medium, + isSingleLineOptions = true, + isFullWidth = true, + options = listOf(stringResource(R.string.dashboardTimeSpentTotal)) + state.courses.map { it.name }, + selectedOption = courseValue, + onOptionSelected = { state.onCourseSelected(it) } + ) + + SingleSelect( + courseSelectState, + modifier = Modifier + .shimmerEffect(isLoading) + .focusable() + ) + } + } + } + } + } +} + +@Composable +private fun DashboardTimeSpentSingleTimeUnit( + timeValue: Int, + timeUnitValue: String, + courseValue: String, + isLoading: Boolean, +) { + val widgetContentDescription = stringResource( + R.string.a11y_dashboardTimeSpentContentDescription, + timeValue, + timeUnitValue, + courseValue + ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clearAndSetSemantics { + contentDescription = widgetContentDescription + } + ) { + Text( + text = timeValue.toString(), + style = HorizonTypography.h1.copy(fontSize = 38.sp, letterSpacing = 0.sp), + color = HorizonColors.Text.body(), + modifier = Modifier + .shimmerEffect(isLoading) + .semantics { + hideFromAccessibility() + } + ) + HorizonSpace(SpaceSize.SPACE_8) + Text( + text = timeUnitValue, + style = HorizonTypography.labelMediumBold, + color = HorizonColors.Text.title(), + modifier = Modifier + .shimmerEffect(isLoading) + .semantics { + hideFromAccessibility() + } + ) + } +} + +@Composable +private fun DashboardTimeSpentMultiTimeUnit( + hours: Int, + minutes: Int, + courseValue: String, + isLoading: Boolean, +) { + val widgetContentDescription = stringResource( + R.string.a11y_dashboardTimeSpentCombinedContentDescription, + hours, + minutes, + courseValue + ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clearAndSetSemantics { + contentDescription = widgetContentDescription + } + ) { + DashboardTimeSpentSingleTimeUnit( + timeValue = hours, + timeUnitValue = pluralStringResource(R.plurals.dashboardTimeSpentHoursShortUnit, hours), + courseValue = courseValue, + isLoading = isLoading + ) + HorizonSpace(SpaceSize.SPACE_8) + DashboardTimeSpentSingleTimeUnit( + timeValue = minutes, + timeUnitValue = pluralStringResource(R.plurals.dashboardTimeSpentMinutesShortUnit, minutes), + courseValue = courseValue, + isLoading = isLoading + ) + } +} + +@Composable +@Preview +private fun DashboardTimeSpentCardContentPreview() { + DashboardTimeSpentCardContent( + state = DashboardTimeSpentCardState( + hours = 20, + courses = listOf( + CourseOption(1, "Introduction to Computer Science"), + CourseOption(2, "Advanced Mathematics"), + CourseOption(3, "Physics 101") + ), + selectedCourseId = null + ), + isLoading = false + ) +} + +@Composable +@Preview +private fun DashboardTimeSpentCardSelectedContentPreview() { + DashboardTimeSpentCardContent( + state = DashboardTimeSpentCardState( + hours = 20, + courses = listOf( + CourseOption(1, "Introduction to Computer Science"), + CourseOption(2, "Advanced Mathematics"), + CourseOption(3, "Physics 101") + ), + selectedCourseId = 1 + ), + isLoading = false + ) +} + +@Composable +@Preview +private fun DashboardTimeSpentCardContentSingleCourseHoursPreview() { + DashboardTimeSpentCardContent( + state = DashboardTimeSpentCardState( + hours = 20, + courses = listOf( + CourseOption(1, "Introduction to Computer Science") + ), + selectedCourseId = null + ), + isLoading = false + ) +} + +@Composable +@Preview +private fun DashboardTimeSpentCardContentSingleCourseMinutesPreview() { + DashboardTimeSpentCardContent( + state = DashboardTimeSpentCardState( + minutes = 20, + courses = listOf( + CourseOption(1, "Introduction to Computer Science") + ), + selectedCourseId = null + ), + isLoading = false + ) +} + +@Composable +@Preview +private fun DashboardTimeSpentCardContentSingleCourseCombinedPreview() { + DashboardTimeSpentCardContent( + state = DashboardTimeSpentCardState( + hours = 10, + minutes = 20, + courses = listOf( + CourseOption(1, "Introduction to Computer Science") + ), + selectedCourseId = null + ), + isLoading = false + ) +} + +@Composable +@Preview +private fun DashboardTimeSpentCardEmptyContentPreview() { + DashboardTimeSpentCardContent( + state = DashboardTimeSpentCardState( + courses = listOf( + CourseOption(1, "Introduction to Computer Science") + ), + selectedCourseId = null + ), + isLoading = false + ) +} + +@Composable +@Preview +private fun DashboardTimeSpentCardLoadingPreview() { + DashboardTimeSpentCardContent( + state = DashboardTimeSpentCardState(), + isLoading = true + ) +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/card/DashboardTimeSpentCardState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/card/DashboardTimeSpentCardState.kt new file mode 100644 index 0000000000..198c52b23d --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/card/DashboardTimeSpentCardState.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.timespent.card + +data class DashboardTimeSpentCardState( + val hours: Int = 0, + val minutes: Int = 0, + val courses: List = emptyList(), + val selectedCourseId: Long? = null, + val onCourseSelected: (String?) -> Unit = {} +) + +data class CourseOption( + val id: Long, + val name: String +) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/home/HomeRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/home/HomeRepository.kt index bef9714d30..41754aee59 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/home/HomeRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/home/HomeRepository.kt @@ -18,8 +18,8 @@ package com.instructure.horizon.features.home import com.instructure.canvasapi2.apis.ThemeAPI import com.instructure.canvasapi2.apis.UserAPI import com.instructure.canvasapi2.builders.RestParams -import com.instructure.canvasapi2.managers.CourseWithProgress -import com.instructure.canvasapi2.managers.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager import com.instructure.canvasapi2.models.CanvasTheme import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.ApiPrefs diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/InboxEventHandler.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/InboxEventHandler.kt new file mode 100644 index 0000000000..a323f22b5e --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/InboxEventHandler.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.horizon.features.inbox + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import javax.inject.Inject +import javax.inject.Singleton + +sealed interface InboxEvent { + data object RefreshRequested : InboxEvent + data object AnnouncementRead : InboxEvent + data class ConversationCreated(val message: String) : InboxEvent +} + +@Singleton +class InboxEventHandler @Inject constructor() { + + private val _events = MutableSharedFlow(replay = 0) + val events = _events.asSharedFlow() + + suspend fun postEvent(event: InboxEvent) { + _events.emit(event) + } +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeScreen.kt index f75aecc6ca..47fc1f97e3 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeScreen.kt @@ -47,6 +47,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat @@ -56,9 +58,6 @@ import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.horizon.R import com.instructure.horizon.features.inbox.attachment.HorizonInboxAttachmentPicker import com.instructure.horizon.features.inbox.attachment.HorizonInboxAttachmentPickerUiState -import com.instructure.horizon.features.inbox.list.HORIZON_INBOX_LIST_NEW_CONVERSATION_CREATED -import com.instructure.horizon.features.inbox.list.HORIZON_REFRESH_INBOX_LIST -import com.instructure.horizon.features.inbox.navigation.HorizonInboxRoute import com.instructure.horizon.horizonui.foundation.HorizonColors import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius import com.instructure.horizon.horizonui.foundation.HorizonElevation @@ -275,7 +274,13 @@ private fun CourseRecipientPickerSection(state: HorizonInboxComposeUiState) { onMenuOpenChanged = { isRecipientPickerOpened = it }, minSearchQueryLengthForMenu = state.minQueryLength ) - MultiSelectSearch(recipientPickerState) + val context = LocalContext.current + MultiSelectSearch( + recipientPickerState, + Modifier.semantics { + contentDescription = context.getString(R.string.a11y_inboxComposeSelectCourse) + } + ) HorizonSpace(SpaceSize.SPACE_12) } @@ -403,23 +408,11 @@ private fun HorizonInboxComposeControlsSection(state: HorizonInboxComposeUiState ) } } else { - val listEntry = remember(navController.currentBackStackEntry) { - try { - navController.getBackStackEntry(HorizonInboxRoute.InboxList.route) - } catch (e: IllegalArgumentException) { - // If the back stack entry doesn't exist, we can safely ignore it - null - } - } Button( label = stringResource(R.string.inboxComposeSendLabel), color = ButtonColor.Institution, onClick = { state.onSendConversation { - listEntry?.savedStateHandle?.set( - HORIZON_REFRESH_INBOX_LIST, - HORIZON_INBOX_LIST_NEW_CONVERSATION_CREATED - ) navController.popBackStack() } } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeViewModel.kt index a9e5650bb5..846ebdc660 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeViewModel.kt @@ -25,6 +25,8 @@ import com.instructure.canvasapi2.models.Recipient import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.horizon.R +import com.instructure.horizon.features.inbox.InboxEvent +import com.instructure.horizon.features.inbox.InboxEventHandler import com.instructure.horizon.features.inbox.attachment.HorizonInboxAttachment import com.instructure.horizon.features.inbox.attachment.HorizonInboxAttachmentState import dagger.hilt.android.lifecycle.HiltViewModel @@ -36,13 +38,15 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @OptIn(FlowPreview::class) @HiltViewModel class HorizonInboxComposeViewModel @Inject constructor( private val repository: HorizonInboxComposeRepository, - @ApplicationContext private val context: Context + @ApplicationContext private val context: Context, + private val inboxEventHandler: InboxEventHandler ): ViewModel() { private val _uiState = MutableStateFlow( HorizonInboxComposeUiState( @@ -151,6 +155,14 @@ class HorizonInboxComposeViewModel @Inject constructor( ) repository.invalidateConversationListCachedResponse() + viewModelScope.launch { + inboxEventHandler.postEvent( + InboxEvent.ConversationCreated( + context.getString(R.string.inboxListConversationCreatedMessage) + ) + ) + } + _uiState.update { it.copy(isSendLoading = false) } onFinished() } catch { diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsRepository.kt index 5c28368500..d50df9d638 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsRepository.kt @@ -28,6 +28,7 @@ import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.DiscussionTopic import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.utils.DataResult +import retrofit2.Call import javax.inject.Inject class HorizonInboxDetailsRepository @Inject constructor( @@ -46,6 +47,11 @@ class HorizonInboxDetailsRepository @Inject constructor( return accountNotificationApi.getAccountNotifications(params, true, true).dataOrThrow.first { it.id == id } } + suspend fun deleteAccountAnnouncement(id: Long): DataResult { + val params = RestParams() + return accountNotificationApi.deleteAccountNotification(id, params) + } + suspend fun getAnnouncement(id: Long, courseId: Long, forceRefresh: Boolean): DiscussionTopicHeader { val params = RestParams(isForceReadFromNetwork = forceRefresh) return announcementApi.getCourseAnnouncement(courseId, id, params).dataOrThrow diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsScreen.kt index c1c35dc62d..b0af64c093 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsScreen.kt @@ -39,7 +39,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -61,9 +60,6 @@ import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.horizon.R import com.instructure.horizon.features.inbox.attachment.HorizonInboxAttachmentPicker import com.instructure.horizon.features.inbox.attachment.HorizonInboxAttachmentPickerViewModel -import com.instructure.horizon.features.inbox.list.HORIZON_INBOX_LIST_ANNOUNCEMENT_READ -import com.instructure.horizon.features.inbox.list.HORIZON_REFRESH_INBOX_LIST -import com.instructure.horizon.features.inbox.navigation.HorizonInboxRoute import com.instructure.horizon.horizonui.foundation.HorizonColors import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius import com.instructure.horizon.horizonui.foundation.HorizonSpace @@ -101,21 +97,6 @@ fun HorizonInboxDetailsScreen( state: HorizonInboxDetailsUiState, navController: NavHostController ) { - val listEntry = remember(navController.currentBackStackEntry) { - try { - navController.getBackStackEntry(HorizonInboxRoute.InboxList.route) - } catch (e: IllegalArgumentException) { - // If the back stack entry doesn't exist, we can safely ignore it - null - } - } - - LaunchedEffect(state.announcementMarkedAsRead) { - if (state.announcementMarkedAsRead) { - listEntry?.savedStateHandle?.set(HORIZON_REFRESH_INBOX_LIST, HORIZON_INBOX_LIST_ANNOUNCEMENT_READ) - } - } - Scaffold( containerColor = HorizonColors.Surface.pagePrimary(), topBar = { HorizonInboxDetailsHeader(state.title, state.titleIcon, state, navController) }, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsUiState.kt index 3ad95155f9..52012b1ba5 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsUiState.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsUiState.kt @@ -30,7 +30,6 @@ data class HorizonInboxDetailsUiState( val items: List = emptyList(), val replyState: HorizonInboxReplyState? = null, val bottomLayout: Boolean = false, - val announcementMarkedAsRead: Boolean = false, ) data class HorizonInboxReplyState( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsViewModel.kt index 362efc7cdc..0beb4050f1 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsViewModel.kt @@ -29,7 +29,11 @@ import com.instructure.canvasapi2.utils.toDate import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardEvent +import com.instructure.horizon.features.dashboard.DashboardEventHandler import com.instructure.horizon.features.inbox.HorizonInboxItemType +import com.instructure.horizon.features.inbox.InboxEvent +import com.instructure.horizon.features.inbox.InboxEventHandler import com.instructure.horizon.features.inbox.attachment.HorizonInboxAttachment import com.instructure.horizon.features.inbox.navigation.HorizonInboxRoute import com.instructure.horizon.horizonui.platform.LoadingState @@ -39,7 +43,6 @@ import com.instructure.pandautils.room.appdatabase.entities.FileDownloadProgress import com.instructure.pandautils.room.appdatabase.entities.FileDownloadProgressState import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -55,6 +58,8 @@ class HorizonInboxDetailsViewModel @Inject constructor( private val workManager: WorkManager, private val fileDownloadProgressDao: FileDownloadProgressDao, savedStateHandle: SavedStateHandle, + private val inboxEventHandler: InboxEventHandler, + private val dashboardEventHandler: DashboardEventHandler, ): ViewModel() { private val courseId: String? = savedStateHandle[HorizonInboxRoute.InboxDetails.COURSE_ID] private val typeStringValue: String? = savedStateHandle[HorizonInboxRoute.InboxDetails.TYPE] @@ -136,6 +141,17 @@ class HorizonInboxDetailsViewModel @Inject constructor( } HorizonInboxItemType.AccountNotification -> { val accountNotification = repository.getAccountAnnouncement(id, forceRefresh) + + if (!accountNotification.closed) { + viewModelScope.tryLaunch { + val result = repository.deleteAccountAnnouncement(id) + if (result.isSuccess) { + inboxEventHandler.postEvent(InboxEvent.AnnouncementRead) + dashboardEventHandler.postEvent(DashboardEvent.AnnouncementRefresh) + } + } catch { } + } + uiState.value.copy( title = accountNotification.subject, titleIcon = R.drawable.campaign, @@ -161,7 +177,7 @@ class HorizonInboxDetailsViewModel @Inject constructor( val topic = repository.getAnnouncementTopic(id, courseId.toLong(), forceRefresh) if (announcement.status == DiscussionTopicHeader.ReadState.UNREAD) { - viewModelScope.async { + viewModelScope.tryLaunch { val result = repository.markAnnouncementAsRead( courseId = courseId.toLong(), announcementId = id, @@ -169,12 +185,10 @@ class HorizonInboxDetailsViewModel @Inject constructor( ) if (result.isSuccess) { - _uiState.update { it.copy(announcementMarkedAsRead = true) } + inboxEventHandler.postEvent(InboxEvent.AnnouncementRead) + dashboardEventHandler.postEvent(DashboardEvent.AnnouncementRefresh) } - - // We need to refresh the announcement in the background, so the next time we open and it's opened from the cache it wouldn't show as unread - repository.getAnnouncement(id, courseId.toLong(), true) - } + } catch { } } uiState.value.copy( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListRepository.kt index dd744aed20..e97a82964a 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListRepository.kt @@ -30,9 +30,6 @@ import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Recipient import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.depaginate -import com.instructure.canvasapi2.utils.toApiString -import java.util.Calendar -import java.util.Date import javax.inject.Inject class HorizonInboxListRepository @Inject constructor( @@ -66,9 +63,6 @@ class HorizonInboxListRepository @Inject constructor( } else { announcementsApi.getFirstPageAnnouncements( courseCode = courses.map { it.contextId }.toTypedArray(), - startDate = Calendar.getInstance() - .apply { set(Calendar.YEAR, get(Calendar.YEAR) - 1) }.time.toApiString(), - endDate = Date().toApiString(), params = params ) .depaginate { announcementsApi.getNextPageAnnouncementsList(it, params) } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListScreen.kt index 7e8a4fef20..ce3e5977c6 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListScreen.kt @@ -87,10 +87,6 @@ import com.instructure.pandautils.utils.getActivityOrNull import com.instructure.pandautils.utils.localisedFormat import java.util.Date -const val HORIZON_REFRESH_INBOX_LIST = "horizon_refresh_inbox_list" -const val HORIZON_INBOX_LIST_NEW_CONVERSATION_CREATED = "horizon_inbox_list_new_conversation_created" -const val HORIZON_INBOX_LIST_ANNOUNCEMENT_READ = "horizon_inbox_list_announcement_read" - @Composable fun HorizonInboxListScreen( state: HorizonInboxListUiState, @@ -112,21 +108,6 @@ fun HorizonInboxListScreen( } } - val listEntry = remember(navController.currentBackStackEntry) { navController.getBackStackEntry(HorizonInboxRoute.InboxList.route) } - val savedStateHandle = listEntry.savedStateHandle - val refreshFlow = remember { savedStateHandle.getStateFlow(HORIZON_REFRESH_INBOX_LIST, null) } - val refreshTrigger by refreshFlow.collectAsState() - val snackbarMessage = stringResource(R.string.inboxListConversationCreatedMessage) - LaunchedEffect(refreshTrigger) { - if (refreshTrigger != null) { - state.loadingState.onRefresh() - if (refreshTrigger == HORIZON_INBOX_LIST_NEW_CONVERSATION_CREATED) { - state.showSnackbar(snackbarMessage) - } - savedStateHandle[HORIZON_REFRESH_INBOX_LIST] = null - } - } - Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, containerColor = HorizonColors.Surface.pagePrimary(), diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListViewModel.kt index 7e7d9dd765..40c8072db1 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListViewModel.kt @@ -29,6 +29,8 @@ import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.horizon.R import com.instructure.horizon.features.inbox.HorizonInboxItemType +import com.instructure.horizon.features.inbox.InboxEvent +import com.instructure.horizon.features.inbox.InboxEventHandler import com.instructure.horizon.horizonui.platform.LoadingState import com.instructure.pandautils.utils.orDefault import dagger.hilt.android.lifecycle.HiltViewModel @@ -40,13 +42,15 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @OptIn(FlowPreview::class) @HiltViewModel class HorizonInboxListViewModel @Inject constructor( @ApplicationContext private val context: Context, - private val repository: HorizonInboxListRepository + private val repository: HorizonInboxListRepository, + private val inboxEventHandler: InboxEventHandler ): ViewModel() { private val _uiState = MutableStateFlow( HorizonInboxListUiState( @@ -81,6 +85,19 @@ class HorizonInboxListViewModel @Inject constructor( } catch { showErrorState() } + + viewModelScope.launch { + inboxEventHandler.events.collect { event -> + when (event) { + is InboxEvent.RefreshRequested -> refresh() + is InboxEvent.AnnouncementRead -> refresh() + is InboxEvent.ConversationCreated -> { + refresh() + showSnackbar(event.message) + } + } + } + } } private fun loadData() { @@ -170,7 +187,7 @@ class HorizonInboxListViewModel @Inject constructor( title = context.getString(R.string.inboxAnnouncementTitle), description = it.subject, date = it.startDate, - isUnread = true + isUnread = !it.closed ) } ) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/navigation/HorizonInboxNavigation.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/navigation/HorizonInboxNavigation.kt index e140b26e95..71a05eee05 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/navigation/HorizonInboxNavigation.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/navigation/HorizonInboxNavigation.kt @@ -160,7 +160,7 @@ fun NavGraphBuilder.horizonInboxNavigation( val courseId = backStackEntry.arguments?.getLong(HorizonInboxRoute.InboxDetails.COURSE_ID) ?: return@LaunchedEffect val id = backStackEntry.arguments?.getLong(HorizonInboxRoute.InboxDetails.ID) ?: return@LaunchedEffect navController.navigate(HorizonInboxRoute.InboxDetails.route(id, HorizonInboxItemType.CourseNotification, courseId)) { - popUpTo(HorizonInboxRoute.InboxDetailsDeepLink.route) { + popUpTo(HorizonInboxRoute.CourseAnnouncementDetailsDeepLink.route) { inclusive = true } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginSignInPageTest.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnEventHandler.kt similarity index 51% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginSignInPageTest.kt rename to libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnEventHandler.kt index 2bdbcdd5d0..5d31ea7f04 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/LoginSignInPageTest.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnEventHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 - present Instructure, Inc. + * Copyright (C) 2025 - present Instructure, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,25 +12,25 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ +package com.instructure.horizon.features.learn + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import javax.inject.Inject +import javax.inject.Singleton -package com.instructure.teacher.ui +sealed interface LearnEvent { + data object RefreshRequested : LearnEvent +} -import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.enterDomain -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test +@Singleton +class LearnEventHandler @Inject constructor() { -@HiltAndroidTest -class LoginSignInPageTest: TeacherTest() { + private val _events = MutableSharedFlow(replay = 0) + val events = _events.asSharedFlow() - // Runs live; no MockCanvas - @Test - override fun displaysPageObjects() { - loginLandingPage.clickFindMySchoolButton() - enterDomain() - loginFindSchoolPage.clickToolbarNextMenuItem() - loginSignInPage.assertPageObjects() + suspend fun postEvent(event: LearnEvent) { + _events.emit(event) } } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnRepository.kt index 6fbba3bdc6..f99559fc0e 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnRepository.kt @@ -15,11 +15,11 @@ */ package com.instructure.horizon.features.learn -import com.instructure.canvasapi2.managers.CourseWithModuleItemDurations -import com.instructure.canvasapi2.managers.CourseWithProgress -import com.instructure.canvasapi2.managers.HorizonGetCoursesManager -import com.instructure.canvasapi2.managers.graphql.JourneyApiManager -import com.instructure.canvasapi2.managers.graphql.Program +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithModuleItemDurations +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetProgramsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Program import com.instructure.canvasapi2.utils.ApiPrefs import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -28,7 +28,7 @@ import javax.inject.Inject class LearnRepository @Inject constructor( private val horizonGetCoursesManager: HorizonGetCoursesManager, - private val journeyApiManager: JourneyApiManager, + private val getProgramsManager: GetProgramsManager, private val apiPrefs: ApiPrefs ) { suspend fun getCoursesWithProgress(forceNetwork: Boolean): List { @@ -37,7 +37,7 @@ class LearnRepository @Inject constructor( } suspend fun getPrograms(forceNetwork: Boolean = false): List { - return journeyApiManager.getPrograms(forceNetwork) + return getProgramsManager.getPrograms(forceNetwork) } suspend fun getCoursesById(courseIds: List, forceNetwork: Boolean = false): List = coroutineScope { diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnScreen.kt index ee2f1fc1cb..89c4de046d 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnScreen.kt @@ -65,7 +65,7 @@ import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import com.instructure.canvasapi2.managers.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.horizon.R import com.instructure.horizon.features.learn.course.CourseDetailsScreen diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnUiState.kt index 9d28a66deb..860abf50c7 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnUiState.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnUiState.kt @@ -15,9 +15,9 @@ */ package com.instructure.horizon.features.learn -import com.instructure.canvasapi2.managers.CourseWithModuleItemDurations -import com.instructure.canvasapi2.managers.CourseWithProgress -import com.instructure.canvasapi2.managers.graphql.Program +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithModuleItemDurations +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Program import com.instructure.horizon.horizonui.platform.LoadingState data class LearnUiState( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnViewModel.kt index 30b8191757..64bc5ff492 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnViewModel.kt @@ -30,6 +30,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -37,6 +38,7 @@ class LearnViewModel @Inject constructor( @ApplicationContext val context: Context, private val repository: LearnRepository, savedStateHandle: SavedStateHandle, + private val learnEventHandler: LearnEventHandler ) : ViewModel() { private val _state = MutableStateFlow( @@ -52,6 +54,14 @@ class LearnViewModel @Inject constructor( init { loadData() + + viewModelScope.launch { + learnEventHandler.events.collect { event -> + when (event) { + is LearnEvent.RefreshRequested -> onRefresh() + } + } + } } private fun loadData() { diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsScreen.kt index ee492f4ed1..62ed59915a 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsScreen.kt @@ -33,7 +33,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import com.instructure.horizon.features.learn.course.lti.CourseToolsScreen @@ -45,6 +49,7 @@ import com.instructure.horizon.horizonui.foundation.HorizonColors import com.instructure.horizon.horizonui.foundation.HorizonTypography import com.instructure.horizon.horizonui.molecules.ProgressBar import com.instructure.horizon.horizonui.organisms.tabrow.TabRow +import com.instructure.horizon.horizonui.selectable import kotlinx.coroutines.launch @Composable @@ -143,12 +148,17 @@ private fun Tab(tab: CourseDetailsTab, isSelected: Boolean, modifier: Modifier = modifier = modifier .padding(bottom = 2.dp) ) { + val context = LocalContext.current Text( stringResource(tab.titleRes), style = HorizonTypography.p1, color = color, modifier = Modifier .padding(top = 20.dp) + .semantics { + role = Role.Tab + selectable(context, isSelected) + } ) } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsUiState.kt index 31295bca4a..5dce9e62ad 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsUiState.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsUiState.kt @@ -17,7 +17,7 @@ package com.instructure.horizon.features.learn.course import androidx.annotation.StringRes -import com.instructure.canvasapi2.managers.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress import com.instructure.horizon.R data class CourseDetailsUiState( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsViewModel.kt index fd90316749..9611fbfc5c 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsViewModel.kt @@ -17,7 +17,7 @@ package com.instructure.horizon.features.learn.course import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.instructure.canvasapi2.managers.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/progress/CourseProgressScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/progress/CourseProgressScreen.kt index 40d45f4e02..3f0ac5602a 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/progress/CourseProgressScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/progress/CourseProgressScreen.kt @@ -31,7 +31,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -42,7 +41,6 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.instructure.canvasapi2.utils.ContextKeeper -import com.instructure.horizon.features.moduleitemsequence.SHOULD_REFRESH_LEARN_SCREEN import com.instructure.horizon.horizonui.foundation.HorizonColors import com.instructure.horizon.horizonui.foundation.HorizonTypography import com.instructure.horizon.horizonui.organisms.cards.ModuleContainer @@ -72,17 +70,6 @@ fun CourseProgressScreen( } } - val parentEntry = remember(mainNavController.currentBackStackEntry) { mainNavController.getBackStackEntry("home") } - val savedStateHandle = parentEntry.savedStateHandle - val refreshFlow = remember { savedStateHandle.getStateFlow(SHOULD_REFRESH_LEARN_SCREEN, false) } - val shouldRefresh by refreshFlow.collectAsState() - LaunchedEffect(shouldRefresh) { - if (shouldRefresh) { - state.screenState.onRefresh() - savedStateHandle[SHOULD_REFRESH_LEARN_SCREEN] = false - } - } - LoadingStateWrapper(state.screenState) { LearnProgressContent( state, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/progress/CourseProgressViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/progress/CourseProgressViewModel.kt index 96c5e23075..29e6e90983 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/progress/CourseProgressViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/progress/CourseProgressViewModel.kt @@ -23,6 +23,8 @@ import com.instructure.canvasapi2.models.ModuleItem import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.horizon.R +import com.instructure.horizon.features.learn.LearnEvent +import com.instructure.horizon.features.learn.LearnEventHandler import com.instructure.horizon.horizonui.organisms.cards.ModuleHeaderStateMapper import com.instructure.horizon.horizonui.organisms.cards.ModuleItemCardStateMapper import com.instructure.horizon.horizonui.platform.LoadingState @@ -32,6 +34,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -40,6 +43,7 @@ class CourseProgressViewModel @Inject constructor( private val repository: CourseProgressRepository, private val moduleHeaderStateMapper: ModuleHeaderStateMapper, private val moduleItemCardStateMapper: ModuleItemCardStateMapper, + private val learnEventHandler: LearnEventHandler ): ViewModel() { private val _uiState = MutableStateFlow( CourseProgressUiState( @@ -51,6 +55,16 @@ class CourseProgressViewModel @Inject constructor( ) val uiState = _uiState.asStateFlow() + init { + viewModelScope.launch { + learnEventHandler.events.collect { event -> + when (event) { + is LearnEvent.RefreshRequested -> refresh() + } + } + } + } + fun loadState(courseId: Long) { _uiState.update { it.copy( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/score/CourseScoreScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/score/CourseScoreScreen.kt index 588d684925..13e0b2f4bd 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/score/CourseScoreScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/score/CourseScoreScreen.kt @@ -14,6 +14,8 @@ * along with this program. If not, see . * */ +@file:OptIn(ExperimentalComposeUiApi::class) + package com.instructure.horizon.features.learn.course.score import androidx.compose.animation.AnimatedContent @@ -41,11 +43,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.invisibleToUser +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -352,28 +358,36 @@ private fun GroupWeightsContent(assignmentGroups: List @Composable private fun GroupWeightItem(assignmentGroup: AssignmentGroupScoreItem) { + val groupWeightText = stringResource( + R.string.weightPercentageValue, + assignmentGroup.groupWeight + ) + val mergedContentDescription = "${assignmentGroup.name}, $groupWeightText" + Column( modifier = Modifier .padding(vertical = 16.dp) + .semantics(mergeDescendants = true) { + contentDescription = mergedContentDescription + } ) { Column ( modifier = Modifier.padding(horizontal = 24.dp), ) { Text( - text = assignmentGroup.name.orEmpty(), + text = assignmentGroup.name, style = HorizonTypography.p1, - color = HorizonColors.Text.body() + color = HorizonColors.Text.body(), + modifier = Modifier.semantics { invisibleToUser() } ) HorizonSpace(SpaceSize.SPACE_8) Text( - text = stringResource( - R.string.weightPercentageValue, - assignmentGroup.groupWeight - ), + text = groupWeightText, style = HorizonTypography.p1, - color = HorizonColors.Text.body() + color = HorizonColors.Text.body(), + modifier = Modifier.semantics { invisibleToUser() } ) } } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsRepository.kt index 94af881e34..59961e4b10 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsRepository.kt @@ -15,10 +15,10 @@ */ package com.instructure.horizon.features.learn.program -import com.instructure.canvasapi2.managers.CourseWithModuleItemDurations -import com.instructure.canvasapi2.managers.HorizonGetCoursesManager -import com.instructure.canvasapi2.managers.graphql.JourneyApiManager -import com.instructure.canvasapi2.managers.graphql.Program +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithModuleItemDurations +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetProgramsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Program import com.instructure.canvasapi2.utils.DataResult import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -26,11 +26,11 @@ import kotlinx.coroutines.coroutineScope import javax.inject.Inject class ProgramDetailsRepository @Inject constructor( - private val journeyApiManager: JourneyApiManager, + private val getProgramsManager: GetProgramsManager, private val getCoursesManager: HorizonGetCoursesManager ) { suspend fun getProgramDetails(programId: String, forceNetwork: Boolean = false): Program { - val program = journeyApiManager.getPrograms(forceNetwork).find { it.id == programId } + val program = getProgramsManager.getPrograms(forceNetwork).find { it.id == programId } ?: throw IllegalArgumentException("Program with id $programId not found") return program } @@ -42,6 +42,6 @@ class ProgramDetailsRepository @Inject constructor( } suspend fun enrollCourse(progressId: String): DataResult { - return journeyApiManager.enrollCourse(progressId) + return getProgramsManager.enrollCourse(progressId) } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsScreen.kt index e789ac8a33..672dbe3311 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsScreen.kt @@ -44,7 +44,6 @@ import com.instructure.horizon.features.learn.program.components.ProgramProgress import com.instructure.horizon.features.learn.program.components.ProgramProgressState import com.instructure.horizon.features.learn.program.components.ProgramsProgressBar import com.instructure.horizon.features.learn.program.components.SequentialProgramProgressProperties -import com.instructure.horizon.features.moduleitemsequence.SHOULD_REFRESH_DASHBOARD import com.instructure.horizon.horizonui.foundation.HorizonColors import com.instructure.horizon.horizonui.foundation.HorizonSpace import com.instructure.horizon.horizonui.foundation.HorizonTypography @@ -66,15 +65,6 @@ fun ProgramDetailsScreen(uiState: ProgramDetailsUiState, mainNavController: NavH } } - val homeEntry = - remember(mainNavController.currentBackStackEntry) { mainNavController.getBackStackEntry(MainNavigationRoute.Home.route) } - LaunchedEffect(uiState.shouldRefreshDashboard) { - if (uiState.shouldRefreshDashboard) { - homeEntry.savedStateHandle[SHOULD_REFRESH_DASHBOARD] = true - uiState.onDashboardRefreshed() - } - } - LoadingStateWrapper(loadingState = uiState.loadingState) { Column( modifier = modifier diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsUiState.kt index cb6c6bd522..890fdcb2f1 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsUiState.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsUiState.kt @@ -28,8 +28,6 @@ data class ProgramDetailsUiState( val programProgressState: ProgramProgressState = ProgramProgressState(courses = emptyList()), val navigateToCourseId: Long? = null, val onNavigateToCourse: () -> Unit = {}, - val shouldRefreshDashboard: Boolean = false, - val onDashboardRefreshed: () -> Unit = {}, ) data class ProgramDetailTag( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsViewModel.kt index c7cba752b1..2405a221da 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/program/ProgramDetailsViewModel.kt @@ -18,12 +18,14 @@ package com.instructure.horizon.features.learn.program import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.instructure.canvasapi2.managers.CourseWithModuleItemDurations -import com.instructure.canvasapi2.managers.graphql.Program -import com.instructure.canvasapi2.managers.graphql.ProgramRequirement +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithModuleItemDurations +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Program +import com.instructure.canvasapi2.managers.graphql.horizon.journey.ProgramRequirement import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.horizon.R +import com.instructure.horizon.features.dashboard.DashboardEvent +import com.instructure.horizon.features.dashboard.DashboardEventHandler import com.instructure.horizon.features.learn.program.components.CourseCardChipState import com.instructure.horizon.features.learn.program.components.CourseCardStatus import com.instructure.horizon.features.learn.program.components.ProgramCourseCardState @@ -49,7 +51,8 @@ import kotlin.time.Duration @HiltViewModel class ProgramDetailsViewModel @Inject constructor( @ApplicationContext private val context: Context, - private val repository: ProgramDetailsRepository + private val repository: ProgramDetailsRepository, + private val dashboardEventHandler: DashboardEventHandler ) : ViewModel() { private val _uiState = MutableStateFlow( @@ -58,8 +61,7 @@ class ProgramDetailsViewModel @Inject constructor( onRefresh = ::refreshProgram, onSnackbarDismiss = ::dismissSnackbar ), - onNavigateToCourse = ::onNavigateToCourse, - onDashboardRefreshed = ::dashboardRefreshed + onNavigateToCourse = ::onNavigateToCourse ) ) val state = _uiState.asStateFlow() @@ -354,7 +356,9 @@ class ProgramDetailsViewModel @Inject constructor( return@launch } try { - _uiState.update { it.copy(shouldRefreshDashboard = true) } + viewModelScope.launch { + dashboardEventHandler.postEvent(DashboardEvent.ProgressRefresh) + } loadData(forceNetwork = true) } catch (e: Exception) { updateCourseEnrollLoadingState(courseId, false) @@ -387,8 +391,4 @@ class ProgramDetailsViewModel @Inject constructor( ) } } - - private fun dashboardRefreshed() { - _uiState.update { it.copy(shouldRefreshDashboard = false) } - } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceRepository.kt index 20338f2938..0423c2d35e 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceRepository.kt @@ -18,7 +18,7 @@ package com.instructure.horizon.features.moduleitemsequence import com.instructure.canvasapi2.apis.AssignmentAPI import com.instructure.canvasapi2.apis.ModuleAPI import com.instructure.canvasapi2.builders.RestParams -import com.instructure.canvasapi2.managers.HorizonGetCommentsManager +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCommentsManager import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.ModuleItem diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt index 15a633f413..a526ca2f46 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt @@ -132,9 +132,6 @@ import com.instructure.pandautils.utils.orDefault import kotlinx.coroutines.launch import kotlin.math.abs -const val SHOULD_REFRESH_DASHBOARD = "shouldRefreshDashboard" -const val SHOULD_REFRESH_LEARN_SCREEN = "shouldRefreshLearnScreen" - @Composable fun ModuleItemSequenceScreen(mainNavController: NavHostController, uiState: ModuleItemSequenceUiState) { val activity = LocalContext.current.getActivityOrNull() @@ -290,15 +287,6 @@ private fun ModuleItemSequenceContent( .padding(top = moduleHeaderHeight) ) { if (uiState.currentPosition != -1) { - val homeEntry = - remember(mainNavController.currentBackStackEntry) { mainNavController.getBackStackEntry(MainNavigationRoute.Home.route) } - LaunchedEffect(uiState.shouldRefreshPreviousScreen) { - if (uiState.shouldRefreshPreviousScreen) { - homeEntry.savedStateHandle[SHOULD_REFRESH_DASHBOARD] = true - homeEntry.savedStateHandle[SHOULD_REFRESH_LEARN_SCREEN] = true - } - } - val pagerState = rememberPagerState(initialPage = uiState.currentPosition, pageCount = { uiState.items.size }) var previousPosition by rememberSaveable { mutableStateOf(uiState.currentPosition) } LaunchedEffect(key1 = uiState.currentPosition) { diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceViewModel.kt index 65f1b2d568..0afcccbe11 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceViewModel.kt @@ -31,6 +31,10 @@ import com.instructure.horizon.R import com.instructure.horizon.features.aiassistant.common.AiAssistContextProvider import com.instructure.horizon.features.aiassistant.common.model.AiAssistContext import com.instructure.horizon.features.aiassistant.common.model.AiAssistContextSource +import com.instructure.horizon.features.dashboard.DashboardEvent +import com.instructure.horizon.features.dashboard.DashboardEventHandler +import com.instructure.horizon.features.learn.LearnEvent +import com.instructure.horizon.features.learn.LearnEventHandler import com.instructure.horizon.features.moduleitemsequence.progress.ProgressPageItem import com.instructure.horizon.features.moduleitemsequence.progress.ProgressPageUiState import com.instructure.horizon.features.moduleitemsequence.progress.ProgressScreenUiState @@ -55,6 +59,8 @@ class ModuleItemSequenceViewModel @Inject constructor( private val moduleItemCardStateMapper: ModuleItemCardStateMapper, private val aiAssistContextProvider: AiAssistContextProvider, savedStateHandle: SavedStateHandle, + private val dashboardEventHandler: DashboardEventHandler, + private val learnEventHandler: LearnEventHandler ) : ViewModel() { private val courseId = savedStateHandle.toRoute().courseId private val moduleItemId = savedStateHandle.toRoute().moduleItemId @@ -577,6 +583,9 @@ class ModuleItemSequenceViewModel @Inject constructor( private fun courseProgressChanged() { courseProgressChanged = true - _uiState.update { it.copy(shouldRefreshPreviousScreen = true) } + viewModelScope.launch { + dashboardEventHandler.postEvent(DashboardEvent.ProgressRefresh) + learnEventHandler.postEvent(LearnEvent.RefreshRequested) + } } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentContentScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentContentScreen.kt index b0aa28016c..f098a41a05 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentContentScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentContentScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -112,7 +113,10 @@ fun AssessmentContentScreen( modifier = Modifier .fillMaxSize() .padding(top = 16.dp) - .background(HorizonColors.Surface.pageSecondary(), shape = HorizonCornerRadius.level5) + .background( + HorizonColors.Surface.pageSecondary(), + shape = HorizonCornerRadius.level5 + ).testTag("AssessmentDialog") ) { Box( modifier = Modifier @@ -136,8 +140,10 @@ fun AssessmentContentScreen( progress = null ) } else { + val context = LocalContext.current IconButton( iconRes = R.drawable.close, + contentDescription = context.getString(R.string.a11y_close), color = IconButtonColor.Inverse, modifier = Modifier .align(Alignment.CenterEnd), diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsRepository.kt index 97d04b039e..21bdfdb775 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsRepository.kt @@ -18,7 +18,7 @@ package com.instructure.horizon.features.moduleitemsequence.content.assignment import com.instructure.canvasapi2.apis.AssignmentAPI import com.instructure.canvasapi2.apis.OAuthAPI import com.instructure.canvasapi2.builders.RestParams -import com.instructure.canvasapi2.managers.HorizonGetCommentsManager +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCommentsManager import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.utils.orDefault diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsViewModel.kt index 9fcf36f260..36aa4729dc 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsViewModel.kt @@ -93,7 +93,7 @@ class AssignmentDetailsViewModel @Inject constructor( emptyList() } val initialAttempt = lastActualSubmission?.attempt ?: 0L // We need to use 0 as the initial attempt if there are no submissions - val description = htmlContentFormatter.formatHtmlWithIframes(assignment.description.orEmpty()) + val description = htmlContentFormatter.formatHtmlWithIframes(assignment.description.orEmpty(), courseId) val attemptsUiState = createAttemptCardsState(attempts, assignment, initialAttempt) val showAttemptSelector = assignment.allowedAttempts != 1L diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsRepository.kt index 820bc27e8b..7cd9f376f5 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsRepository.kt @@ -17,8 +17,8 @@ package com.instructure.horizon.features.moduleitemsequence.content.assignment.c import com.instructure.canvasapi2.apis.SubmissionAPI import com.instructure.canvasapi2.builders.RestParams -import com.instructure.canvasapi2.managers.CommentsData -import com.instructure.canvasapi2.managers.HorizonGetCommentsManager +import com.instructure.canvasapi2.managers.graphql.horizon.CommentsData +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCommentsManager import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.utils.DataResult import javax.inject.Inject diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsViewModel.kt index 0bc6a6b475..d9ff6b5ff2 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsViewModel.kt @@ -20,8 +20,8 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.work.WorkManager -import com.instructure.canvasapi2.managers.CommentAttachment -import com.instructure.canvasapi2.managers.CommentsData +import com.instructure.canvasapi2.managers.graphql.horizon.CommentAttachment +import com.instructure.canvasapi2.managers.graphql.horizon.CommentsData import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch @@ -200,10 +200,10 @@ class CommentsViewModel @Inject constructor( viewModelScope.tryLaunch { commentsRepository.postComment(courseId, assignmentId, apiPrefs.user?.id.orDefault(), attempt, commentText).dataOrThrow - reloadData() _uiState.update { it.copy(comment = TextFieldValue(""), postingComment = false) } + reloadData() } catch { _ -> _uiState.update { it.copy(postingComment = false, errorMessage = context.getString(R.string.commentsBottomSheet_failedToPostComment)) } } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsContentScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsContentScreen.kt index fff15f61bd..25e20ddd92 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsContentScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsContentScreen.kt @@ -30,9 +30,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import com.instructure.canvasapi2.managers.NoteHighlightedData -import com.instructure.canvasapi2.managers.NoteHighlightedDataRange -import com.instructure.canvasapi2.managers.NoteHighlightedDataTextPosition +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataRange +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataTextPosition import com.instructure.horizon.features.aiassistant.common.model.AiAssistContextSource import com.instructure.horizon.features.notebook.common.webview.ComposeNotesHighlightingCanvasWebView import com.instructure.horizon.features.notebook.common.webview.NotesCallback diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsRepository.kt index abdd1a0464..5399a28e5d 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsRepository.kt @@ -19,7 +19,7 @@ import com.apollographql.apollo.api.Optional import com.instructure.canvasapi2.apis.OAuthAPI import com.instructure.canvasapi2.apis.PageAPI import com.instructure.canvasapi2.builders.RestParams -import com.instructure.canvasapi2.managers.RedwoodApiManager +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.RedwoodApiManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Page import com.instructure.horizon.features.notebook.common.model.Note diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsUiState.kt index 59caae8f31..5d2d7131f7 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsUiState.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsUiState.kt @@ -15,7 +15,7 @@ */ package com.instructure.horizon.features.moduleitemsequence.content.page -import com.instructure.canvasapi2.managers.NoteHighlightedData +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData import com.instructure.horizon.features.notebook.common.model.Note import com.instructure.horizon.horizonui.platform.LoadingState diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsViewModel.kt index 594b460d2e..99740bd22d 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsViewModel.kt @@ -18,7 +18,7 @@ package com.instructure.horizon.features.moduleitemsequence.content.page import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.instructure.canvasapi2.managers.NoteHighlightedData +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.horizon.features.moduleitemsequence.ModuleItemContent @@ -64,7 +64,7 @@ class PageDetailsViewModel @Inject constructor( it.copy(loadingState = it.loadingState.copy(isLoading = true)) } val pageDetails = pageDetailsRepository.getPageDetails(courseId, pageUrl) - val html = htmlContentFormatter.formatHtmlWithIframes(pageDetails.body.orEmpty()) + val html = htmlContentFormatter.formatHtmlWithIframes(pageDetails.body.orEmpty(), courseId) val notes = try { // We don't want to fail the page load if fetching notes fails pageDetailsRepository.getNotes(courseId, pageDetails.id) } catch (e: Exception) { diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookRepository.kt index c1ee6d8efd..ecd66a204b 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookRepository.kt @@ -17,7 +17,7 @@ package com.instructure.horizon.features.notebook import com.apollographql.apollo.api.Optional -import com.instructure.canvasapi2.managers.RedwoodApiManager +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.RedwoodApiManager import com.instructure.horizon.features.notebook.common.model.NotebookType import com.instructure.redwood.QueryNotesQuery import com.instructure.redwood.type.LearningObjectFilter diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookScreen.kt index d8b565f66e..fdf3723dae 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookScreen.kt @@ -39,10 +39,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import com.instructure.canvasapi2.managers.NoteHighlightedData -import com.instructure.canvasapi2.managers.NoteHighlightedDataRange -import com.instructure.canvasapi2.managers.NoteHighlightedDataTextPosition -import com.instructure.canvasapi2.managers.NoteObjectType +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataRange +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataTextPosition +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteObjectType import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.horizon.R import com.instructure.horizon.features.notebook.common.composable.NotebookAppBar diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteScreen.kt index 24d99ba66f..4efc360ecf 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteScreen.kt @@ -37,9 +37,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.navigation.NavHostController -import com.instructure.canvasapi2.managers.NoteHighlightedData -import com.instructure.canvasapi2.managers.NoteHighlightedDataRange -import com.instructure.canvasapi2.managers.NoteHighlightedDataTextPosition +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataRange +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataTextPosition import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.horizon.R import com.instructure.horizon.features.notebook.common.composable.NotebookAppBar diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteUiState.kt index c27cdb90b7..b543e57d2e 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteUiState.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteUiState.kt @@ -17,7 +17,7 @@ package com.instructure.horizon.features.notebook.addedit import androidx.compose.ui.text.input.TextFieldValue -import com.instructure.canvasapi2.managers.NoteHighlightedData +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData import com.instructure.horizon.features.notebook.common.model.NotebookType data class AddEditNoteUiState( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteRepository.kt index 92e23c484a..7d9f6d73a7 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteRepository.kt @@ -16,8 +16,8 @@ */ package com.instructure.horizon.features.notebook.addedit.add -import com.instructure.canvasapi2.managers.NoteHighlightedData -import com.instructure.canvasapi2.managers.RedwoodApiManager +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.RedwoodApiManager import com.instructure.horizon.features.notebook.common.model.NotebookType import javax.inject.Inject diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteViewModel.kt index 7358f16de7..264b1e2c17 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteViewModel.kt @@ -22,9 +22,9 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute -import com.instructure.canvasapi2.managers.NoteHighlightedData -import com.instructure.canvasapi2.managers.NoteHighlightedDataRange -import com.instructure.canvasapi2.managers.NoteHighlightedDataTextPosition +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataRange +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataTextPosition import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.horizon.R diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/edit/EditNoteRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/edit/EditNoteRepository.kt index 635ccc5beb..76fe2bcb68 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/edit/EditNoteRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/edit/EditNoteRepository.kt @@ -16,8 +16,8 @@ */ package com.instructure.horizon.features.notebook.addedit.edit -import com.instructure.canvasapi2.managers.NoteHighlightedData -import com.instructure.canvasapi2.managers.RedwoodApiManager +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.RedwoodApiManager import com.instructure.horizon.features.notebook.common.model.NotebookType import javax.inject.Inject diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/edit/EditNoteViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/edit/EditNoteViewModel.kt index ace8df1ab0..7173d62db6 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/edit/EditNoteViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/edit/EditNoteViewModel.kt @@ -22,9 +22,9 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute -import com.instructure.canvasapi2.managers.NoteHighlightedData -import com.instructure.canvasapi2.managers.NoteHighlightedDataRange -import com.instructure.canvasapi2.managers.NoteHighlightedDataTextPosition +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataRange +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataTextPosition import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.horizon.R diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/model/Note.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/model/Note.kt index dfcd8bf273..6011400e2e 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/model/Note.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/model/Note.kt @@ -1,11 +1,11 @@ package com.instructure.horizon.features.notebook.common.model import com.google.gson.Gson -import com.instructure.canvasapi2.managers.NoteHighlightedData -import com.instructure.canvasapi2.managers.NoteHighlightedDataRange -import com.instructure.canvasapi2.managers.NoteHighlightedDataTextPosition -import com.instructure.canvasapi2.managers.NoteObjectType -import com.instructure.canvasapi2.managers.NoteReaction +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataRange +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataTextPosition +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteObjectType +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteReaction import com.instructure.pandautils.utils.toJson import com.instructure.redwood.QueryNotesQuery import java.util.Date diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationRepository.kt index 4b8ebc923a..0c0285925f 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationRepository.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationRepository.kt @@ -19,24 +19,44 @@ package com.instructure.horizon.features.notification import com.instructure.canvasapi2.apis.CourseAPI import com.instructure.canvasapi2.apis.StreamAPI import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.StreamItem +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.depaginate import javax.inject.Inject class NotificationRepository @Inject constructor( + private val apiPrefs: ApiPrefs, private val streamApi: StreamAPI.StreamInterface, - private val courseApi: CourseAPI.CoursesInterface + private val courseApi: CourseAPI.CoursesInterface, + private val getCoursesManager: HorizonGetCoursesManager ) { suspend fun getNotifications(forceRefresh: Boolean): List { + val courseIds = getCoursesInProgress(forceRefresh) val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceRefresh) return streamApi.getUserStream(restParams) .depaginate { streamApi.getNextPageStream(it, restParams) } .dataOrThrow + .filter { + it.courseId == -1L || courseIds.contains(it.courseId) + } + .filter { + it.isDueDateNotification() + || it.isNotificationItemScored() + || it.isGradingPeriodNotification() + } } suspend fun getCourse(courseId: Long): Course { val restParams = RestParams() return courseApi.getCourse(courseId, restParams).dataOrThrow } + + private suspend fun getCoursesInProgress(forceRefresh: Boolean): List { + return getCoursesManager + .getCoursesWithProgress(apiPrefs.user?.id ?: -1L, forceRefresh) + .dataOrThrow + .map { it.courseId } + } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationScreen.kt index e72198b7e0..8c27503c8a 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationScreen.kt @@ -16,136 +16,202 @@ */ package com.instructure.horizon.features.notification +import android.content.Context import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import androidx.navigation.NavDeepLinkRequest +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.horizon.R +import com.instructure.horizon.horizonui.foundation.HorizonBorder import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius import com.instructure.horizon.horizonui.foundation.HorizonSpace import com.instructure.horizon.horizonui.foundation.HorizonTypography import com.instructure.horizon.horizonui.foundation.SpaceSize -import com.instructure.horizon.horizonui.molecules.HorizonDivider -import com.instructure.horizon.horizonui.molecules.IconButton -import com.instructure.horizon.horizonui.molecules.IconButtonColor +import com.instructure.horizon.horizonui.molecules.Badge +import com.instructure.horizon.horizonui.molecules.BadgeContent +import com.instructure.horizon.horizonui.molecules.BadgeType +import com.instructure.horizon.horizonui.molecules.StatusChip +import com.instructure.horizon.horizonui.molecules.StatusChipColor +import com.instructure.horizon.horizonui.molecules.StatusChipState import com.instructure.horizon.horizonui.organisms.scaffolds.HorizonScaffold +import com.instructure.horizon.horizonui.platform.LoadingState import com.instructure.horizon.horizonui.platform.LoadingStateWrapper +import com.instructure.pandautils.utils.ViewStyler +import com.instructure.pandautils.utils.getActivityOrNull +import com.instructure.pandautils.utils.isPreviousDay +import com.instructure.pandautils.utils.isSameDay +import com.instructure.pandautils.utils.isSameWeek +import com.instructure.pandautils.utils.localisedFormat +import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @Composable -fun NotificationScreen(state: NotificationUiState, mainNavController: NavController) { +fun NotificationScreen(state: NotificationUiState, mainNavController: NavHostController) { + val activity = LocalContext.current.getActivityOrNull() + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + LaunchedEffect(Unit) { + if (activity != null) ViewStyler.setStatusBarColor(activity, ContextCompat.getColor(activity, R.color.surface_pagePrimary)) + } + HorizonScaffold( title = stringResource(R.string.notificationsTitle), + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, onBackPressed = { mainNavController.popBackStack() }, ) { modifier -> LoadingStateWrapper(state.screenState) { - NotificationContent(state, modifier) + NotificationContent( + mainNavController, + state, + showSnackbar = { message -> + scope.launch { snackbarHostState.showSnackbar(message) } + }, + modifier + ) } } } @Composable -private fun NotificationContent(state: NotificationUiState, modifier: Modifier = Modifier) { +private fun NotificationContent( + navController: NavHostController, + state: NotificationUiState, + showSnackbar: (String) -> Unit, + modifier: Modifier = Modifier +) { Column( modifier = modifier.background(HorizonColors.Surface.pageSecondary()) ) { LazyColumn( - contentPadding = PaddingValues(top = 16.dp), + contentPadding = PaddingValues(top = 16.dp, bottom = 8.dp), modifier = Modifier .weight(1f) ) { - if (state.allNotificationItems.isEmpty()) { + if (state.notificationItems.isEmpty()) { item { EmptyNotificationItemContent() } } else { - items(state.pagedNotificationItems[state.currentPageIndex]) { item -> - Column { - NotificationItemContent( - categoryLabel = item.categoryLabel, - title = item.title, - date = item.date - ) - - HorizonDivider() - } + items(state.notificationItems) { item -> + NotificationItemContent(navController, item, showSnackbar) } } } - - HorizonDivider() - - if (state.pagedNotificationItems.size > 1) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp) - ) { - IconButton( - iconRes = R.drawable.chevron_left, - onClick = state.decreasePageIndex, - color = IconButtonColor.Black, - enabled = state.currentPageIndex > 0 && state.pagedNotificationItems.size > 1 - ) - - HorizonSpace(SpaceSize.SPACE_8) - - IconButton( - iconRes = R.drawable.chevron_right, - onClick = state.increasePageIndex, - color = IconButtonColor.Black, - enabled = state.currentPageIndex < state.pagedNotificationItems.lastIndex - ) - } - } } } @Composable private fun EmptyNotificationItemContent() { - NotificationItemContent( - categoryLabel = "", - title = stringResource(R.string.notificationsEmptyMessage), - date = "" + Text( + stringResource(R.string.notificationsEmptyMessage), + style = HorizonTypography.p1, + color = HorizonColors.Text.body(), + modifier = Modifier + .padding(horizontal = 24.dp, vertical = 8.dp) ) } @Composable private fun NotificationItemContent( - categoryLabel: String, - title: String, - date: String, + navController: NavHostController, + notificationItem: NotificationItem, + showSnackbar: (String) -> Unit ) { + val context = LocalContext.current Column( modifier = Modifier - .padding(horizontal = 24.dp, vertical = 16.dp) + .padding(horizontal = 24.dp, vertical = 8.dp) + .border( + HorizonBorder.level2(HorizonColors.LineAndBorder.lineStroke()), + HorizonCornerRadius.level2 + ) + .clip(HorizonCornerRadius.level2) + .fillMaxWidth() + .clickable { + val request = NavDeepLinkRequest.Builder + .fromUri(notificationItem.deepLink.toUri()) + .build() + + try { + navController.navigate(request) + } catch (e: IllegalArgumentException) { + showSnackbar(context.getString(R.string.notificationsFailedToOpenMessage)) + } + } + .padding(horizontal = 16.dp) ) { - Text( - text = categoryLabel, - style = HorizonTypography.labelSmallBold, - color = HorizonColors.Text.timestamp() - ) + HorizonSpace(SpaceSize.SPACE_16) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + StatusChip( + state = StatusChipState( + label = notificationItem.category.label, + color = notificationItem.category.color, + fill = true + ) + ) - HorizonSpace(SpaceSize.SPACE_4) + Spacer(modifier = Modifier.weight(1f)) + + if (!notificationItem.isRead){ + Badge( + content = BadgeContent.ColorSmall, + type = BadgeType.Custom( + backgroundColor = HorizonColors.Surface.inversePrimary(), + contentColor = HorizonColors.Surface.institution() + ) + ) + } + } + HorizonSpace(SpaceSize.SPACE_8) + + if (notificationItem.courseLabel != null) { + Text( + text = notificationItem.courseLabel, + style = HorizonTypography.p3, + color = HorizonColors.Text.timestamp() + ) + HorizonSpace(SpaceSize.SPACE_8) + } Text( - text = title, + text = notificationItem.title, style = HorizonTypography.p1, color = HorizonColors.Text.body(), maxLines = 2, @@ -155,9 +221,61 @@ private fun NotificationItemContent( HorizonSpace(SpaceSize.SPACE_4) Text( - text = date, - style = HorizonTypography.labelSmall, + text = notificationItem.date.toLocalisedFormat(LocalContext.current), + style = HorizonTypography.p3, color = HorizonColors.Text.timestamp() ) + HorizonSpace(SpaceSize.SPACE_16) + } +} + +private fun Date?.toLocalisedFormat(context: Context): String { + if (this == null) return "" + if (this.isSameDay(Date())) { + return context.getString(R.string.notificationsDateToday) } + if (this.isPreviousDay(Date())) { + return context.getString(R.string.notificationsDateYesterday) + } + if (this.isSameWeek(Date())) { + return SimpleDateFormat("EEEE", Locale.getDefault()).format(this) + } + return this.localisedFormat("MMM dd, yyyy") +} + +@Composable +@Preview +private fun NotificationsScreenPreview() { + ContextKeeper.appContext = LocalContext.current + val sampleNotifications = listOf( + NotificationItem( + category = NotificationItemCategory( + stringResource(R.string.notificationsDueDateCategoryLabel), + StatusChipColor.Honey + ), + title = "Your assignment is due soon", + courseLabel = "Biology 101", + date = Date(), + isRead = false, + deepLink = "myapp://course/1/assignment/1" + ), + NotificationItem( + category = NotificationItemCategory( + stringResource(R.string.notificationsScoreChangedCategoryLabel), + StatusChipColor.Violet + ), + title = "Your score has been updated", + courseLabel = "Math 201", + date = Calendar.getInstance().apply { time = Date(); add(Calendar.DAY_OF_WEEK, -1) }.time, + isRead = true, + deepLink = "course/2/grade" + ), + ) + + val previewState = NotificationUiState( + screenState = LoadingState(), + notificationItems = sampleNotifications + ) + + NotificationContent(navController = rememberNavController(), state = previewState, {}) } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationUiState.kt index 4091298e7c..4073961aa8 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationUiState.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationUiState.kt @@ -1,18 +1,24 @@ package com.instructure.horizon.features.notification +import com.instructure.horizon.horizonui.molecules.StatusChipColor import com.instructure.horizon.horizonui.platform.LoadingState +import java.util.Date data class NotificationUiState( val screenState: LoadingState, - val allNotificationItems: List = emptyList(), - val pagedNotificationItems: List> = emptyList(), - val currentPageIndex: Int = 0, - val decreasePageIndex: () -> Unit = {}, - val increasePageIndex: () -> Unit = {}, + val notificationItems: List = emptyList(), ) data class NotificationItem( - val categoryLabel: String, + val category: NotificationItemCategory, + val courseLabel: String?, val title: String, - val date: String, + val date: Date?, + val isRead: Boolean, + val deepLink: String, +) + +data class NotificationItemCategory( + val label: String, + val color: StatusChipColor, ) \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationViewModel.kt index a626ac06ee..4c9f244d2b 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationViewModel.kt @@ -19,14 +19,12 @@ package com.instructure.horizon.features.notification import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.StreamItem import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.horizon.R +import com.instructure.horizon.horizonui.molecules.StatusChipColor import com.instructure.horizon.horizonui.platform.LoadingState -import com.instructure.pandautils.utils.localisedFormat -import com.instructure.pandautils.utils.orDefault import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow @@ -45,8 +43,6 @@ class NotificationViewModel @Inject constructor( onRefresh = ::refresh, onSnackbarDismiss = ::dismissSnackbar ), - decreasePageIndex = ::decreasePageIndex, - increasePageIndex = ::increasePageIndex, ) ) val uiState = _uiState.asStateFlow() @@ -74,18 +70,24 @@ class NotificationViewModel @Inject constructor( } private suspend fun loadData(forceRefresh: Boolean = false) { - val notifications = repository.getNotifications(forceRefresh) - val items = notifications.map { + val userNotifications = repository.getNotifications(forceRefresh).map { NotificationItem( - categoryLabel = getNotificationItemCategoryLabel(it), + category = getNotificationItemCategoryLabel(it), title = getNotificationItemTitle(it), - date = it.updatedDate?.localisedFormat("MMM dd").orEmpty() + courseLabel = if (it.isCourseNotification()) getCourseName(it.courseId) else null, + date = it.updatedDate, + isRead = it.isReadState, + deepLink = if (it.assignment?.htmlUrl != null) { + it.assignment?.htmlUrl!! + } else { + it.htmlUrl + } ) } + _uiState.update { it.copy( - allNotificationItems = items, - pagedNotificationItems = items.chunked(10), + notificationItems = (userNotifications).sortedByDescending { item -> item.date } ) } } @@ -108,54 +110,47 @@ class NotificationViewModel @Inject constructor( } } - private suspend fun getNotificationItemCategoryLabel(streamItem: StreamItem): String { - val courseName = repository.getCourse(streamItem.courseId).name - if (isNotificationItemScored(streamItem)) { - return context.getString(R.string.notificationsAssignmentScoredCategoryLabel) + private fun getNotificationItemCategoryLabel(streamItem: StreamItem): NotificationItemCategory { + if (streamItem.isNotificationItemScored()) { + return NotificationItemCategory( + context.getString(R.string.notificationsScoreChangedCategoryLabel), + StatusChipColor.Violet + ) } - if (isDueDateChanged(streamItem)) { - return context.getString(R.string.notificationsDueDateChangedCategoryLabel) + if (streamItem.isGradingPeriodNotification()) { + return NotificationItemCategory( + context.getString(R.string.notificationsScoreCategoryLabel), + StatusChipColor.Violet + ) } - if (isGradingWeightChanged(streamItem)) { - return context.getString(R.string.notificationsScoringWeightChangedCategoryLabel) + if (streamItem.isDueDateNotification()) { + return NotificationItemCategory( + context.getString(R.string.notificationsDueDateCategoryLabel), + StatusChipColor.Honey + ) } - if (streamItem.contextType == CanvasContext.Type.COURSE) { - return context.getString( - R.string.notificationsAnnouncementFromCetegoryLabel, - courseName + if (streamItem.isCourseNotification()) { + return NotificationItemCategory( + context.getString(R.string.notificationsAnnouncementCategoryLabel), + StatusChipColor.Sky ) } - return streamItem.notificationCategory + return NotificationItemCategory( + streamItem.notificationCategory, + StatusChipColor.Honey + ) } - private suspend fun getNotificationItemTitle(streamItem: StreamItem): String { - val courseName = repository.getCourse(streamItem.courseId).name - if (isNotificationItemScored(streamItem)) { + private fun getNotificationItemTitle(streamItem: StreamItem): String { + if (streamItem.isNotificationItemScored()) { return context.getString(R.string.notificationsScoredItemTitle, streamItem.title) } - if (isDueDateChanged(streamItem)) { - return formatDueDateTitle(streamItem, courseName) - } - if (isAssignmentCreated(streamItem)) { - return streamItem.title?.replace(", $courseName", "").orEmpty() - } - if (isGradingWeightChanged(streamItem)) { - return formatGradingWeightChangeTitle(streamItem) - } return streamItem.title.orEmpty() } - private fun increasePageIndex() { - _uiState.update { - it.copy(currentPageIndex = it.currentPageIndex + 1) - } - } - - private fun decreasePageIndex() { - _uiState.update { - it.copy(currentPageIndex = it.currentPageIndex - 1) - } + private suspend fun getCourseName(courseId: Long): String { + return repository.getCourse(courseId).name } private fun dismissSnackbar() { @@ -163,42 +158,4 @@ class NotificationViewModel @Inject constructor( it.copy(screenState = it.screenState.copy(snackbarMessage = null)) } } - - // TODO: There is no API support for handling categories and titles. - // TODO: For now the Web and iOS logic is copied, but it won't work if language support is introduced. - // TODO: This should be handled in the API. - private fun isNotificationItemScored(streamItem: StreamItem): Boolean { - return streamItem.grade != null || streamItem.score != -1.0 - } - - private fun isDueDateChanged(streamItem: StreamItem): Boolean { - return streamItem.notificationCategory == "Due Date" - && streamItem.title?.contains("Assignment Due Date Changed").orDefault() - } - - private fun isAssignmentCreated(streamItem: StreamItem): Boolean { - return streamItem.notificationCategory == "Due Date" - && streamItem.title?.contains("Assignment Created").orDefault() - } - - private fun isGradingWeightChanged(streamItem: StreamItem): Boolean { - return streamItem.notificationCategory == "Grading Policies" - || streamItem.title?.contains("Grading Weight Changed").orDefault() - } - - private fun formatDueDateTitle(streamItem: StreamItem, courseName: String): String { - val assignmentName = streamItem.title - ?.replace("Assignment Due Date Changed: ", "") - ?.replace(", $courseName", "") - - val dateComponent = streamItem.getMessage(context)?.split("\n\n")?.firstOrNull() - val date = dateComponent.orEmpty() - - return context.getString(R.string.notificationsDueOnTitle, assignmentName, date) - } - - private fun formatGradingWeightChangeTitle(streamItem: StreamItem): String { - val courseNameFromTitle = streamItem.title?.replace("Grade Weight Changed: ", "") - return context.getString(R.string.notificationsScoreWeightChangedTitle, courseNameFromTitle) - } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/StreamItemExtensions.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/StreamItemExtensions.kt new file mode 100644 index 0000000000..242475e0b7 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/StreamItemExtensions.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.notification + +import com.instructure.canvasapi2.models.StreamItem + +internal fun StreamItem.isNotificationItemScored(): Boolean { + return this.grade != null || this.score != -1.0 +} + +internal fun StreamItem.isDueDateNotification(): Boolean { + return this.notificationCategory == "Due Date" +} + +internal fun StreamItem.isCourseNotification(): Boolean { + return this.type == "Announcement" +} + +internal fun StreamItem.isGradingPeriodNotification(): Boolean { + return this.notificationCategory == "Grading Policies" +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/skillspace/SkillspaceRepository.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/skillspace/SkillspaceRepository.kt new file mode 100644 index 0000000000..16be4b3459 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/skillspace/SkillspaceRepository.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.skillspace + +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.utils.ApiPrefs +import javax.inject.Inject + +class SkillspaceRepository @Inject constructor( + private val oAuthApi: OAuthAPI.OAuthInterface, + private val apiPrefs: ApiPrefs, +) { + suspend fun getAuthenticatedSession(): AuthenticatedSession? { + return oAuthApi.getAuthenticatedSession( + apiPrefs.fullDomain, + RestParams(isForceReadFromNetwork = true) + ).dataOrNull + } + + fun getEmbeddedSkillspaceUrl(): String { + val baseUrl = apiPrefs.fullDomain + val skillspaceUrl = "$baseUrl/career/skillspace" + return "$skillspaceUrl?embedded=true" + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/skillspace/SkillspaceViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/skillspace/SkillspaceViewModel.kt index 564ac354d9..a97846797f 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/skillspace/SkillspaceViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/skillspace/SkillspaceViewModel.kt @@ -18,7 +18,6 @@ package com.instructure.horizon.features.skillspace import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.horizon.horizonui.platform.LoadingState @@ -30,7 +29,7 @@ import javax.inject.Inject @HiltViewModel class SkillspaceViewModel @Inject constructor( - private val apiPrefs: ApiPrefs + private val repository: SkillspaceRepository, ): ViewModel() { private val _uiState = MutableStateFlow(SkillspaceUiState( loadingState = LoadingState(onRefresh = ::refreshData) @@ -61,12 +60,14 @@ class SkillspaceViewModel @Inject constructor( } } - private fun fetchUrl() { - val baseUrl = apiPrefs.fullDomain.toHorizonUrl() - val skillspaceUrl = "$baseUrl/skillspace" - val embeddedUrl = "$skillspaceUrl?embedded=true" + private suspend fun fetchUrl() { + repository.getAuthenticatedSession()?.sessionUrl?.let { url -> + _uiState.update { it.copy(webviewUrl = url) } + } + repository.getEmbeddedSkillspaceUrl().let { url -> + _uiState.update { it.copy(webviewUrl = url) } + } - _uiState.update { it.copy(webviewUrl = embeddedUrl) } } private fun refreshData() { @@ -88,10 +89,4 @@ class SkillspaceViewModel @Inject constructor( } } } - - private fun String.toHorizonUrl(): String { - return this - .replace("horizon.cd.instructure.com", "dev.cd.canvashorizon.com") - .replace("instructure.com", "canvasforcareer.com") - } } \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/Extensions.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/Extensions.kt new file mode 100644 index 0000000000..dea6e9850e --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/Extensions.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.horizon.horizonui + +import android.content.Context +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.SemanticsPropertyReceiver +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.stateDescription +import com.instructure.horizon.R + +fun SemanticsPropertyReceiver.expandable(context: Context, expanded: Boolean) { + val expandedStateDesc = context.getString(R.string.a11y_expanded) + val collapsedStateDesc = context.getString(R.string.a11y_collapsed) + val expandActionLabel = context.getString(R.string.a11y_expand) + val collapseActionLabel = context.getString(R.string.a11y_collapse) + + stateDescription = if (expanded) expandedStateDesc else collapsedStateDesc + liveRegion = LiveRegionMode.Assertive + onClick(if (expanded) collapseActionLabel else expandActionLabel) { false } +} + +fun SemanticsPropertyReceiver.selectable(context: Context, selected: Boolean) { + val selectedStateDesc = context.getString(R.string.a11y_selected) + val unselectedStateDesc = context.getString(R.string.a11y_unselected) + + stateDescription = if (selected) selectedStateDesc else unselectedStateDesc + liveRegion = LiveRegionMode.Assertive +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/animation/ShimmerAnimation.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/animation/ShimmerAnimation.kt new file mode 100644 index 0000000000..56f9148370 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/animation/ShimmerAnimation.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.horizonui.animation + +import androidx.annotation.FloatRange +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.unit.IntSize +import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius + + +@Composable +fun Modifier.shimmerEffect( + enabled: Boolean, + iterationDurationMillis: Int = 1000, + shape: Shape = HorizonCornerRadius.level1, + backgroundColor: Color = HorizonColors.PrimitivesGrey.grey14().copy(alpha = 0.5f), + shimmerColor: Color = HorizonColors.PrimitivesGrey.grey12().copy(alpha = 0.5f), + @FloatRange(from = 0.0, to = 1.0) shimmerRatio: Float = 0.5f, +): Modifier { + if (!enabled) return this + var size by remember { + mutableStateOf(IntSize.Zero) + } + val transition = rememberInfiniteTransition("ShimmerAnimationTransition") + + val startOffsetX by transition.animateFloat( + initialValue = -size.width.toFloat(), + targetValue = size.width.toFloat(), + animationSpec = infiniteRepeatable( + animation = tween(iterationDurationMillis) + ), + label = "ShimmerAnimation" + ) + + val backgroundFirstPart = (1 / shimmerRatio / 2).toInt() + val backgroundSecondPart = ((1 / shimmerRatio) - backgroundFirstPart).toInt() + val colors = buildList { + repeat(backgroundFirstPart) { add(backgroundColor) } + add(shimmerColor) + repeat(backgroundSecondPart) { add(backgroundColor) } + } + + return clip(shape).drawWithContent { + drawRect( + brush = Brush.linearGradient( + colors = colors, + start = Offset(startOffsetX, 0f), + end = Offset(startOffsetX + size.width.toFloat(), size.height.toFloat()) + ) + ) + } + .onGloballyPositioned { + size = it.size + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/foundation/HorizonColors.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/foundation/HorizonColors.kt index bda4805f4a..af8f653b7c 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/foundation/HorizonColors.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/foundation/HorizonColors.kt @@ -266,6 +266,7 @@ object HorizonColors { } object PrimitivesSky { + val sky12 = Color(0xFFDDECF3) @Composable fun sky30() = colorResource(R.color.primitives_sky30) @Composable @@ -282,6 +283,7 @@ object HorizonColors { fun sky70() = colorResource(R.color.primitives_sky70) @Composable fun sky90() = colorResource(R.color.primitives_sky90) + val sky90 = Color(0xFF0F4E6A) @Composable fun sky110() = colorResource(R.color.primitives_sky110) } @@ -298,6 +300,7 @@ object HorizonColors { } object PrimitivesViolet { + val violet12 = Color(0xFFF1E6F5) @Composable fun violet30() = colorResource(R.color.primitives_violet30) @Composable @@ -314,6 +317,7 @@ object HorizonColors { fun violet70() = colorResource(R.color.primitives_violet70) @Composable fun violet90() = colorResource(R.color.primitives_violet90) + val violet90 = Color(0xFF682F82) @Composable fun violet110() = colorResource(R.color.primitives_violet110) } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Button.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Button.kt index f90f853df8..ba2d5cecd8 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Button.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Button.kt @@ -195,7 +195,7 @@ fun LoadingButton( label = label, height = height, width = width, - color = ButtonColor.Institution, + color = color, iconPosition = iconPosition, onClick = onClick, enabled = enabled, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/ProgressBar.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/ProgressBar.kt index aef9a07254..8daa019bac 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/ProgressBar.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/ProgressBar.kt @@ -105,6 +105,12 @@ sealed class ProgressBarStyle( data class WhiteBackground(val overrideProgressColor: Color = HorizonColors.Surface.pageSecondary()) : ProgressBarStyle(HorizonColors.Text.title(), overrideProgressColor, HorizonColors.Surface.pageSecondary()) + + data object Institution: ProgressBarStyle( + HorizonColors.Surface.institution(), + HorizonColors.Surface.institution(), + HorizonColors.Surface.institution().copy(alpha = 0.1f) + ) } @Composable @@ -115,7 +121,7 @@ fun ProgressBarSmall( style: ProgressBarStyle = ProgressBarStyle.Dark(), showLabels: Boolean = true ) { - Column { + Column(modifier) { if (showLabels) { Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = modifier.fillMaxWidth()) { if (label != null) { @@ -133,7 +139,7 @@ fun ProgressBarSmall( } } Box( - modifier + Modifier .background(shape = HorizonCornerRadius.level1, color = style.backgroundColor) .fillMaxWidth() .height(8.dp) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Spinner.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Spinner.kt index c745d23b23..02e40c5173 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Spinner.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/Spinner.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -46,7 +47,8 @@ fun Spinner( ) { val strokeBackground = if (hasStrokeBackground) HorizonColors.LineAndBorder.lineDivider() else Color.Transparent Box( - modifier = modifier, + modifier = modifier + .testTag("LoadingSpinner"), contentAlignment = Alignment.Center ) { if (progress != null) { diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/StatusChip.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/StatusChip.kt index 542d9d721e..a7445de04b 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/StatusChip.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/molecules/StatusChip.kt @@ -71,6 +71,16 @@ sealed class StatusChipColor(val contentColor: Color, val fillColor: Color = Col contentColor = HorizonColors.Text.title(), fillColor = HorizonColors.Surface.pageSecondary() ) + + data object Sky : StatusChipColor( + contentColor = HorizonColors.PrimitivesSky.sky90, + fillColor = HorizonColors.PrimitivesSky.sky12 + ) + + data object Violet : StatusChipColor( + contentColor = HorizonColors.PrimitivesViolet.violet90, + fillColor = HorizonColors.PrimitivesViolet.violet12 + ) } @Composable @@ -113,6 +123,24 @@ private fun StatusChipPreviewGreen() { ) } +@Composable +@Preview(showBackground = true) +private fun StatusChipPreviewSky() { + StatusChipPreview( + color = StatusChipColor.Sky, + iconRes = R.drawable.check_circle_full + ) +} + +@Composable +@Preview(showBackground = true) +private fun StatusChipPreviewViolet() { + StatusChipPreview( + color = StatusChipColor.Violet, + iconRes = null + ) +} + @Composable @Preview(showBackground = true) private fun StatusChipPreviewGrey() { @@ -150,7 +178,7 @@ private fun StatusChipPreviewWhite() { } @Composable -private fun StatusChipPreview(color: StatusChipColor, iconRes: Int) { +private fun StatusChipPreview(color: StatusChipColor, iconRes: Int?) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { StatusChip( state = StatusChipState( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/AnimatedHorizontalPager.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/AnimatedHorizontalPager.kt new file mode 100644 index 0000000000..d3a5840282 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/AnimatedHorizontalPager.kt @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.horizonui.organisms + +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.OverscrollEffect +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.TargetedFlingBehavior +import androidx.compose.foundation.gestures.snapping.SnapPosition +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PageSize +import androidx.compose.foundation.pager.PagerDefaults +import androidx.compose.foundation.pager.PagerScope +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.rememberOverscrollEffect +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.instructure.horizon.horizonui.foundation.HorizonColors +import kotlin.math.abs + +@Composable +fun AnimatedHorizontalPager( + pagerState: PagerState, + modifier: Modifier = Modifier, + sizeAnimationRange: Float = 0.2f, + contentPadding: PaddingValues = PaddingValues(0.dp), + pageSize: PageSize = PageSize.Fill, + beyondViewportPageCount: Int = PagerDefaults.BeyondViewportPageCount, + pageSpacing: Dp = 0.dp, + verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, + flingBehavior: TargetedFlingBehavior = PagerDefaults.flingBehavior(pagerState), + userScrollEnabled: Boolean = true, + reverseLayout: Boolean = false, + key: ((Int) -> Any)? = null, + pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection(pagerState, Orientation.Horizontal), + snapPosition: SnapPosition = SnapPosition.Start, + overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(), + pageContent: @Composable (PagerScope.(Int, Modifier) -> Unit), +) { + HorizontalPager( + pagerState, + contentPadding = contentPadding, + pageSize = pageSize, + beyondViewportPageCount = beyondViewportPageCount, + pageSpacing = pageSpacing, + verticalAlignment = verticalAlignment, + flingBehavior = flingBehavior, + userScrollEnabled = userScrollEnabled, + reverseLayout = reverseLayout, + key = key, + pageNestedScrollConnection = pageNestedScrollConnection, + snapPosition = snapPosition, + overscrollEffect = overscrollEffect, + modifier = modifier.semantics { + role = Role.Carousel + }.animateContentSize() + ) { + var cardWidthList by remember { mutableStateOf(emptyMap()) } + val scaleAnimation by animateFloatAsState( + if (it == pagerState.currentPage) { + (1 - abs(pagerState.currentPageOffsetFraction.convertScaleRange(sizeAnimationRange))) + } else { + (1f - (sizeAnimationRange * 2)) + (abs(pagerState.currentPageOffsetFraction.convertScaleRange(sizeAnimationRange))) + }, + label = "DashboardCourseCardAnimation", + ) + val animationDirection = when { + it < pagerState.currentPage -> 1 + it > pagerState.currentPage -> -1 + else -> if (pagerState.currentPageOffsetFraction > 0) 1 else -1 + } + pageContent( + it, + Modifier + .onGloballyPositioned { coordinates -> + cardWidthList = cardWidthList + (it to coordinates.size.width.toFloat()) + } + .offset { + IntOffset( + (animationDirection * ((cardWidthList[it] + ?: 0f) / 2 * (1 - scaleAnimation))).toInt(), + 0 + ) + } + .scale(scaleAnimation) + ) + } +} + +@Composable +fun AnimatedHorizontalPagerIndicator( + pagerState: PagerState +) { + val selectedIndex = pagerState.currentPage + val offset = pagerState.currentPageOffsetFraction + + var scrollToIndex: Int? by remember { mutableStateOf(null) } + LaunchedEffect(scrollToIndex) { + if (scrollToIndex != null) { + pagerState.animateScrollToPage(scrollToIndex ?: return@LaunchedEffect) + scrollToIndex = null + } + } + + LazyRow( + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .clearAndSetSemantics { + hideFromAccessibility() + } + ) { + items(pagerState.pageCount) { itemIndex -> + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(20.dp) + .padding(5.dp) + .border(1.dp, HorizonColors.Icon.medium(), CircleShape) + .clip(CircleShape) + .clickable { scrollToIndex = itemIndex } + .clearAndSetSemantics { + hideFromAccessibility() + } + ) { + if (itemIndex == selectedIndex) { + Box( + modifier = Modifier + .size(10.dp * (1 - abs(offset))) + .clip(CircleShape) + .background(HorizonColors.Icon.medium()) + ) + } else if (itemIndex == selectedIndex + (1 * if (offset > 0) 1 else -1)) { + Box( + modifier = Modifier + .size(10.dp * (abs(offset))) + .clip(CircleShape) + .background(HorizonColors.Icon.medium()) + ) + } + } + } + } +} + +private fun Float.convertScaleRange(newScale: Float): Float { + val oldMin = -0.5f + val oldMax = 0.5f + val newMin = -newScale + val newMax = newScale + return ((this - oldMin) / (oldMax - oldMin) ) * (newMax - newMin) + newMin +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/CollapsableHeaderScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/CollapsableHeaderScreen.kt new file mode 100644 index 0000000000..865ac2dfc0 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/CollapsableHeaderScreen.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.horizonui.organisms + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max + +@Composable +fun CollapsableHeaderScreen( + headerContent: @Composable () -> Unit, + bodyContent: @Composable () -> Unit, + modifier: Modifier = Modifier +) { + + val scrollConnectionSaver = Saver, Pair>( + save = { it.value.appBarMaxHeight to it.value.appBarOffset }, + restore = { (mutableStateOf(CollapsingAppBarNestedScrollConnection(it.first).apply { appBarOffset = it.second })) } + ) + val density = LocalDensity.current + var moduleHeaderHeight by rememberSaveable { mutableIntStateOf(0) } + var nestedScrollConnection by rememberSaveable(saver = scrollConnectionSaver) { mutableStateOf(CollapsingAppBarNestedScrollConnection(moduleHeaderHeight)) } + + Box( + modifier = modifier + .nestedScroll(nestedScrollConnection) + ) { + Box( + modifier = Modifier + .offset { IntOffset(0, nestedScrollConnection.appBarOffset) } + .onGloballyPositioned { coordinates -> + if (coordinates.size.height != moduleHeaderHeight) { + moduleHeaderHeight = coordinates.size.height + val temp = nestedScrollConnection.appBarOffset + nestedScrollConnection = + CollapsingAppBarNestedScrollConnection(moduleHeaderHeight).apply { appBarOffset = temp } + } + } + ) { + headerContent() + } + val moduleHeaderHeight = max(0.dp, with(density) { moduleHeaderHeight.toDp() } + with(density) { nestedScrollConnection.appBarOffset.toDp() }) + + Box( + modifier = Modifier + .padding(top = moduleHeaderHeight) + ) { + bodyContent() + } + } +} + +private class CollapsingAppBarNestedScrollConnection( + val appBarMaxHeight: Int +) : NestedScrollConnection { + + var appBarOffset: Int by mutableIntStateOf(0) + + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val delta = available.y.toInt() + val newOffset = appBarOffset + delta + val previousOffset = appBarOffset + appBarOffset = newOffset.coerceIn(-appBarMaxHeight, 0) + val consumed = appBarOffset - previousOffset + return Offset(0f, consumed.toFloat()) + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/CollapsableContentCard.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/CollapsableContentCard.kt index 7ec7ec7c8f..5db2a4128f 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/CollapsableContentCard.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/CollapsableContentCard.kt @@ -32,19 +32,25 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.invisibleToUser +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.instructure.horizon.R +import com.instructure.horizon.horizonui.expandable import com.instructure.horizon.horizonui.foundation.HorizonColors import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius import com.instructure.horizon.horizonui.foundation.HorizonSpace import com.instructure.horizon.horizonui.foundation.HorizonTypography import com.instructure.horizon.horizonui.foundation.SpaceSize +@OptIn(ExperimentalComposeUiApi::class) @Composable fun CollapsableContentCard( title: String, @@ -65,13 +71,18 @@ fun CollapsableContentCard( .padding(vertical = 16.dp) ) { Column( - modifier = Modifier.clickable { onExpandChanged(!expanded) } + modifier = Modifier + .clickable { onExpandChanged(!expanded) } + .semantics { + invisibleToUser() + } ){ Text( title, style = HorizonTypography.h2, color = HorizonColors.Text.body(), - modifier = Modifier.padding(horizontal = 24.dp) + modifier = Modifier + .padding(horizontal = 24.dp) ) HorizonSpace(SpaceSize.SPACE_16) @@ -80,11 +91,15 @@ fun CollapsableContentCard( targetValue = if (expanded) 180f else 0f, label = "rotationAnimation" ) - + val context = LocalContext.current Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 24.dp) + .semantics(mergeDescendants = true) { + expandable(context, expanded) + } + ) { Icon( painter = painterResource(R.drawable.keyboard_arrow_down), diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleContainer.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleContainer.kt index ed4da4be80..216f527a79 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleContainer.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleContainer.kt @@ -49,11 +49,13 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.horizon.R +import com.instructure.horizon.horizonui.expandable import com.instructure.horizon.horizonui.foundation.HorizonColors import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius import com.instructure.horizon.horizonui.foundation.HorizonSpace @@ -98,8 +100,13 @@ fun ModuleContainer(state: ModuleHeaderState, modifier: Modifier = Modifier, con Column { val onClick = state.onClick val clickModifier = if (onClick != null) Modifier.clickable { onClick() } else Modifier + val context = LocalContext.current - Column(modifier = clickModifier.padding(16.dp)) { + Column(modifier = clickModifier + .semantics(mergeDescendants = true) { + expandable(context, state.expanded) + } + .padding(16.dp)) { ModuleHeader(state = state) if (state.subtitle != null && state.expanded) { HorizonSpace(SpaceSize.SPACE_24) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleItemCard.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleItemCard.kt index da427604fc..5bdb5e5e96 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleItemCard.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/cards/ModuleItemCard.kt @@ -118,7 +118,7 @@ private fun RowScope.ModuleItemCardIcon(state: ModuleItemCardState, modifier: Mo HorizonSpace(SpaceSize.SPACE_8) Icon( painterResource(R.drawable.lock), - contentDescription = null, + contentDescription = stringResource(R.string.a11y_locked), tint = HorizonColors.Surface.institution(), modifier = modifier ) @@ -128,7 +128,7 @@ private fun RowScope.ModuleItemCardIcon(state: ModuleItemCardState, modifier: Mo HorizonSpace(SpaceSize.SPACE_8) Icon( painterResource(R.drawable.check_circle_full), - contentDescription = null, + contentDescription = stringResource(R.string.a11y_completed), tint = HorizonColors.Surface.institution(), modifier = modifier ) @@ -138,7 +138,7 @@ private fun RowScope.ModuleItemCardIcon(state: ModuleItemCardState, modifier: Mo HorizonSpace(SpaceSize.SPACE_8) Icon( painterResource(R.drawable.circle), - contentDescription = null, + contentDescription = stringResource(R.string.a11y_not_completed), tint = HorizonColors.LineAndBorder.lineStroke(), modifier = modifier ) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/common/InputDropDownPopup.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/common/InputDropDownPopup.kt index 938594952d..f0fe2e226e 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/common/InputDropDownPopup.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/common/InputDropDownPopup.kt @@ -58,7 +58,7 @@ fun InputDropDownPopup( isMenuOpen: Boolean, options: List, verticalOffsetPx: Int, - width: Dp, + width: Dp?, onMenuOpenChanged: (Boolean) -> Unit, onOptionSelected: (T) -> Unit, modifier: Modifier = Modifier, @@ -89,7 +89,12 @@ fun InputDropDownPopup( modifier = modifier .padding(bottom = 8.dp) .padding(horizontal = 8.dp) - .width(width), + .conditional(width == null) { + fillMaxWidth() + } + .conditional(width != null) { + width(width!!) + }, shape = HorizonCornerRadius.level2, colors = CardDefaults.cardColors() .copy(containerColor = HorizonColors.Surface.pageSecondary()), diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/singleselect/SingleSelect.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/singleselect/SingleSelect.kt index 043ff8284b..1f606993e2 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/singleselect/SingleSelect.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/singleselect/SingleSelect.kt @@ -20,7 +20,6 @@ import androidx.compose.animation.core.animateIntAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon @@ -39,6 +38,13 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.instructure.canvasapi2.utils.ContextKeeper @@ -55,7 +61,8 @@ fun SingleSelect( state: SingleSelectState, modifier: Modifier = Modifier ) { - + val expandedState = stringResource(R.string.a11y_expanded) + val collapsedState = stringResource(R.string.a11y_collapsed) Input( label = state.label, helperText = state.helperText, @@ -65,6 +72,15 @@ fun SingleSelect( .onFocusChanged { state.onFocusChanged(it.isFocused) } + .clearAndSetSemantics { + role = Role.DropdownList + stateDescription = if (state.isMenuOpen) expandedState else collapsedState + contentDescription = if (state.selectedOption != null) { + "${state.label}, ${state.selectedOption}" + } else { + state.label ?: "" + } + } ) { Column( modifier = Modifier @@ -89,7 +105,7 @@ fun SingleSelect( InputDropDownPopup( isMenuOpen = state.isMenuOpen, options = state.options, - width = width, + width = if (state.isFullWidth) null else width, verticalOffsetPx = heightInPx, onMenuOpenChanged = state.onMenuOpenChanged, onOptionSelected = { selectedOption -> @@ -138,18 +154,23 @@ private fun SingleSelectContent(state: SingleSelectState) { if (state.selectedOption != null) { Text( text = state.selectedOption, + maxLines = if (state.isSingleLineOptions) 1 else Int.MAX_VALUE, + overflow = TextOverflow.Ellipsis, style = HorizonTypography.p1, color = HorizonColors.Text.body(), + modifier = Modifier.weight(1f) ) } else if (state.placeHolderText != null) { Text( text = state.placeHolderText, + maxLines = if (state.isSingleLineOptions) 1 else Int.MAX_VALUE, + overflow = TextOverflow.Ellipsis, style = HorizonTypography.p1, color = HorizonColors.Text.placeholder(), + modifier = Modifier.weight(1f) ) } - Spacer(modifier = Modifier.weight(1f)) Icon( painter = painterResource(R.drawable.keyboard_arrow_down), diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/singleselect/SingleSelectState.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/singleselect/SingleSelectState.kt index 3276529ec0..6aac34ad05 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/singleselect/SingleSelectState.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/inputs/singleselect/SingleSelectState.kt @@ -10,6 +10,8 @@ data class SingleSelectState( val enabled: Boolean = true, val isMenuOpen: Boolean = false, val errorText: String? = null, + val isSingleLineOptions: Boolean = false, + val isFullWidth: Boolean = false, val required: InputLabelRequired = InputLabelRequired.Regular, val size: SingleSelectInputSize, val options: List, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/scaffolds/HorizonScaffold.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/scaffolds/HorizonScaffold.kt index 356b647a49..814f47b373 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/scaffolds/HorizonScaffold.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/scaffolds/HorizonScaffold.kt @@ -36,10 +36,12 @@ import com.instructure.horizon.horizonui.organisms.topappbar.HorizonTopAppBar fun HorizonScaffold( title: String, onBackPressed: () -> Unit, + snackbarHost: @Composable () -> Unit = {}, content: @Composable (Modifier) -> Unit, ) { Scaffold( topBar = { HorizonTopAppBar(title, onBackPressed) }, + snackbarHost = snackbarHost, contentColor = HorizonColors.Surface.pagePrimary() ) { innerPadding -> Box( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/util/DynamicDeserializer.kt b/libs/horizon/src/main/java/com/instructure/horizon/util/DynamicDeserializer.kt new file mode 100644 index 0000000000..feaff3e5f3 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/util/DynamicDeserializer.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.util + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +val gson = Gson() + +/** + * Deserializes the dynamic list of maps into a list of a specific data class using Gson. + */ +inline fun List.deserializeDynamicList(): List { + val targetType = object : TypeToken() {}.type + + return this.mapNotNull { rawItem -> + if (rawItem is Map<*, *>) { + try { + return@mapNotNull gson.fromJson(gson.toJsonTree(rawItem), targetType) + } catch (e: Exception) { + null + } + } else { + null + } + } +} + +/** + * Deserializes a dynamic object into a specific data class using Gson. + */ +inline fun Any.deserializeDynamicObject(): T? { + val targetType = object : TypeToken() {}.type + + return try { + gson.fromJson(gson.toJsonTree(this), targetType) + } catch (e: Exception) { + null + } +} \ No newline at end of file diff --git a/libs/horizon/src/main/res/drawable/trending_up.xml b/libs/horizon/src/main/res/drawable/trending_up.xml new file mode 100644 index 0000000000..90b313e6c7 --- /dev/null +++ b/libs/horizon/src/main/res/drawable/trending_up.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/libs/horizon/src/main/res/values-ar/strings.xml b/libs/horizon/src/main/res/values-ar/strings.xml index e803ccdf71..8a038748d1 100644 --- a/libs/horizon/src/main/res/values-ar/strings.xml +++ b/libs/horizon/src/main/res/values-ar/strings.xml @@ -71,6 +71,7 @@ Ų†Ø¸ØąØŠ ØšØ§Ų…ØŠ Ø§Ų„Ø¯ØąØŦاØĒ Ø§Ų„Ų…Ų„Ø§Ø­Ø¸Ø§ØĒ + Ø§Ų„ØŖØ¯ŲˆØ§ØĒ Ø§ØŗŲ… Ø§Ų„Ų…Ų‡Ų…ØŠ (Ų…Ų† Ø§Ų„ØŖŲ„Ų ØĨŲ„Ų‰ Ø§Ų„ŲŠØ§ØĄ) ØĒØ§ØąŲŠØŽ Ø§Ų„Ø§ØŗØĒØ­Ų‚Ø§Ų‚ (Ø§Ų„ØŖØ­Ø¯ØĢ ØŖŲˆŲ„Ø§Ų‹) Ø§Ų„Ø§ØŗŲ…: %1$s @@ -187,9 +188,10 @@ ŲØ´Ų„ ØĒØ­Ø¯ŲŠØĢ Ø§Ų„ØĨØšŲ„Ø§Ų…Ø§ØĒ ŲØ´Ų„ ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØĨØšŲ„Ø§Ų…Ø§ØĒ ØĒØĒŲˆŲØą Ø§Ų„ØĸŲ† Ø¯ØąØŦØŠ %1$s - ØĒŲ… Ų…Ų†Ø­ Ø¯ØąØŦØŠ Ų„Ų„Ų…Ų‡Ų…ØŠ - ØĒŲ… ØĒØēŲŠŲŠØą ØĒØ§ØąŲŠØŽ Ø§Ų„Ø§ØŗØĒØ­Ų‚Ø§Ų‚ - ØĒŲ… ØĒØēŲŠŲŠØą ØĒŲˆØ˛ŲŠØš Ø§Ų„Ø¯ØąØŦاØĒ + ØĒŲ… ØĒØēŲŠŲŠØą Ø§Ų„Ø¯ØąØŦØŠ + ØĒØ§ØąŲŠØŽ Ø§Ų„Ø§ØŗØĒØ­Ų‚Ø§Ų‚ + Ø§Ų„Ø¯ØąØŦØŠ + Ø§Ų„ØĨØšŲ„Ø§Ų† ØĨØšŲ„Ø§Ų† Ų…Ų† %1$s ØĒŲ… ØĒØēŲŠŲŠØą ØĒŲ‚ŲŠŲŠŲ… Ø¯ØąØŦاØĒ %1$s ؊ØĒŲ… Ø§ØŗØĒØ­Ų‚Ø§Ų‚ %1$s ؁؊ %2$s @@ -370,4 +372,35 @@ Ø§Ų„ŲƒŲ„ ØŦØ˛ØĄ Ų…Ų† %s Ų„Ø§ ŲŠŲ…ŲƒŲ† ؁ØĒØ­ Ø§Ų„Ų…ØŗØ§Ų‚. + ØŦØ˛ØĄ Ų…Ų† %s + Ų…ØąØ­Ø¨Ų‹Ø§! Ø§ØšØąØļ Ø¨ØąŲ†Ø§Ų…ØŦ؃ Ų„Ų„ØĒØŗØŦŲŠŲ„ ؁؊ Ø§Ų„Ų…ØŗØ§Ų‚ Ø§Ų„ØŖŲˆŲ„ Ų„Ųƒ. + Ø¨ØąŲ†Ø§Ų…ØŦ؊ + ŲØ´Ų„ Ø§Ų„ØĒØŗØŦŲŠŲ„ ؁؊ Ø§Ų„Ø¯ŲˆØąØŠ Ø§Ų„ØĒØ¯ØąŲŠØ¨ŲŠØŠ. + ŲØ´Ų„ ØĒØ­Ų…ŲŠŲ„ Ø§Ų„Ø¨ØąØ§Ų…ØŦ ŲˆØ§Ų„Ų…ØŗØ§Ų‚Ø§ØĒ. + ŲØ´Ų„ ØĒØ­Ø¯ŲŠØĢ Ø§Ų„Ø¨ØąØ§Ų…ØŦ ŲˆØ§Ų„Ų…ØŗØ§Ų‚Ø§ØĒ. + Ø§Ų„ŲŠŲˆŲ… + ØŖŲ…Øŗ + ؊ØĒØšØ°Øą ؁ØĒØ­ Ø§Ų„ØĨØšŲ„Ø§Ų… + Ų„Ø§ ØĒ؈ØŦد ØŖØ¯ŲˆØ§ØĒ ØŽØ§ØąØŦŲŠØŠ Ų„Ų‡Ø°Ø§ Ø§Ų„Ų…ØŗØ§Ų‚. + Ø¯ŲØĒØą Ų…Ų„Ø§Ø­Ø¸Ø§ØĒ + Ø§Ų„ØĨØšŲ„Ø§Ų…Ø§ØĒ + ØšŲ„Ø¨ØŠ Ø§Ų„ŲˆØ§ØąØ¯ + Ø¨ØˇØ§Ų‚ØŠ Ø§Ų„Ų…ØŗØ§Ų‚ %1$d Ų…Ų† %2$d + Ø§ŲƒØĒŲ…Ų„ %1$d\%% + Ų„Ų… Ų†ØĒŲ…ŲƒŲ† Ų…Ų† ØĒØ­Ų…ŲŠŲ„ Ų‡Ø°Ø§ Ø§Ų„Ų…Ø­ØĒŲˆŲ‰. + ŲŠØąØŦŲ‰ ØĨؚاد؊ Ø§Ų„Ų…Ø­Ø§ŲˆŲ„ØŠ. + ØĨؚاد؊ Ø§Ų„Ų…Ø­Ø§ŲˆŲ„ØŠ + Ų…ØąØ­Ø¨Ų‹Ø§! Ø§ØšØąØļ Ø¨ØąŲ†Ø§Ų…ØŦ؃ Ų„Ų„ØĒØŗØŦŲŠŲ„ ؁؊ Ø§Ų„Ų…ØŗØ§Ų‚ Ø§Ų„ØŖŲˆŲ„ Ų„Ųƒ. + ØĒŲØ§ØĩŲŠŲ„ Ø§Ų„Ø¨ØąŲ†Ø§Ų…ØŦ + ØĒŲ‡Ø§Ų†ŲŠŲ†Ø§! Ų„Ų‚Ø¯ Ų‚Ų…ØĒ بØĨŲƒŲ…Ø§Ų„ Ø§Ų„Ų…ØŗØ§Ų‚ Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ. Ø§ØŗØĒØšØąØļ ØĒŲ‚Ø¯Ų…Ųƒ ŲˆØ¯ØąØŦاØĒ؃ ؁؊ ØĩŲØ­ØŠ "Ø§Ų„ØĒØšŲ„Ų…". + ØĒØ­Ø¯ŲŠØ¯ Ų…ØŗØ§Ų‚ + ØĨØēŲ„Ø§Ų‚ + ØĒŲ… Ø§Ų„ØĒŲˆØŗŲŠØš + ØĒŲ… Ø§Ų„ØˇŲŠ + ØĒŲˆØŗŲŠØš + ØˇŲŠ + ØĒŲ… Ø§Ų„ØĨŲƒŲ…Ø§Ų„ + ØēŲŠØą Ų…ŲƒØĒŲ…Ų„ + Ų…Ø¤Ų…Ų‘Ų† + ØĒŲ… ØĨŲ„ØēØ§ØĄ Ø§Ų„ØĒØ­Ø¯ŲŠØ¯ diff --git a/libs/horizon/src/main/res/values-b+da+DK+instk12/strings.xml b/libs/horizon/src/main/res/values-b+da+DK+instk12/strings.xml index a6fc320539..f6a0d2d046 100644 --- a/libs/horizon/src/main/res/values-b+da+DK+instk12/strings.xml +++ b/libs/horizon/src/main/res/values-b+da+DK+instk12/strings.xml @@ -67,6 +67,7 @@ Oversigt Resultater Noter + VÃĻrktøjer Opgavenavn (A-Å) Forfaldsdato (nyeste først) Navn: %1$s @@ -171,9 +172,10 @@ Meddelelser kunne ikke opdateres Meddelelser kunne ikke indlÃĻses %1$s\s resultat er nu tilgÃĻngeligt - Opgavens resultat - Afleveringsdato ÃĻndret - Resultatets vÃĻgtning er blevet ÃĻndret + Resultatet blev ÃĻndret + Afleveringsdato + Resultat + Besked Meddelelse fra %1$s VÃĻgtning af %1$s\s resultat blev ÃĻndret %1$s skal afleveres før den %2$s @@ -346,4 +348,35 @@ Alle En del af %s Faget kan ikke ÃĨbnes. + En del af %s + Velkommen! Se dit program for at tilmelde dig dit første fag. + Mit program + Kunne ikke tilmelde til faget. + Kunne ikke indlÃĻse programmer og fag. + Kunne ikke opdatere programmer og fag. + I dag + I gÃĨr + Kan ikke ÃĨbne meddelelsen + Der findes ingen eksterne vÃĻrktøjer til dette fag. + Notesbog + Notifikationer + Indbakke + Fagkort %1$d af %2$d + %1$d\%% afsluttet + Vi kunne ikke indlÃĻse dette indhold. + Prøv igen. + Prøv igen + Velkommen! Se dit program for at tilmelde dig dit første fag. + Programdetaljer + Tillykke! Du har afsluttet dit fag. Se dine fremskridt og resultater pÃĨ siden LÃĻr. + VÃĻlg fag + Luk + Udvidet + Skjult + Udvid + Skjul + Fuldført + Ikke afsluttet + LÃĨst + Ikke valgt diff --git a/libs/horizon/src/main/res/values-b+en+AU+unimelb/strings.xml b/libs/horizon/src/main/res/values-b+en+AU+unimelb/strings.xml index f9ee8cfe9c..1d4dce5360 100644 --- a/libs/horizon/src/main/res/values-b+en+AU+unimelb/strings.xml +++ b/libs/horizon/src/main/res/values-b+en+AU+unimelb/strings.xml @@ -67,6 +67,7 @@ Overview Scores Notes + Tools Assignment name (A-Z) Due date (newest first) Name: %1$s @@ -171,9 +172,10 @@ Failed to refresh notifications Failed to load notifications %1$s\'s score is now available - Assignment scored - Due date changed - Scoring weight changed + Score changed + Due date + Score + Announcement Announcement from %1$s %1$s\'s score weight was changed %1$s is due on %2$s @@ -346,4 +348,35 @@ All Part of %s The subject cannot be opened. + Part of %s + Welcome! View your program to enrol in your first subject. + My program + Failed to enrol in subject. + Failed to load programs and subjects. + Failed to refresh programs and subjects. + Today + Yesterday + Unable to open Notification + There are no external tools for this subject. + Notebook + Notifications + Inbox + Subject Card %1$d of %2$d + %1$d\%% complete + We weren’t able to load this content. + Please try again. + Retry + Welcome! View your program to enrol in your first subject. + Program details + Congrats! You’ve completed your subject. View your progress and scores on the Learn page. + Select Subject + Close + Expanded + Collapsed + Expand + Collapse + Completed + Not completed + Locked + Unselected diff --git a/libs/horizon/src/main/res/values-b+en+GB+instukhe/strings.xml b/libs/horizon/src/main/res/values-b+en+GB+instukhe/strings.xml index 91f6bd4224..ff4565981d 100644 --- a/libs/horizon/src/main/res/values-b+en+GB+instukhe/strings.xml +++ b/libs/horizon/src/main/res/values-b+en+GB+instukhe/strings.xml @@ -67,6 +67,7 @@ Overview Scores Notes + Tools Assignment name (A-Z) Due date (newest first) Name: %1$s @@ -171,9 +172,10 @@ Failed to refresh notifications Failed to load notifications %1$s\'s score is now available - Assignment scored - Due date changed - Scoring weight changed + Score changed + Due date + Score + Announcement Announcement from %1$s %1$s\'s score weight was changed %1$s is due on %2$s @@ -346,4 +348,35 @@ All Part of %s The module cannot be opened. + Part of %s + Welcome! View your program to enrol in your first module. + My program + Failed to enrol in module. + Failed to load programs and modules. + Failed to refresh programs and modules. + Today + Yesterday + Unable to open Notification + There are no external tools for this module. + Notebook + Notifications + Inbox + Module Card %1$d of %2$d + %1$d\%% complete + We weren’t able to load this content. + Please try again. + Retry + Welcome! View your program to enrol in your first module. + Program details + Congrats! You’ve completed your module. View your progress and scores on the Learn page. + Select Module + Close + Expanded + Collapsed + Expand + Collapse + Completed + Not completed + Locked + Unselected diff --git a/libs/horizon/src/main/res/values-b+nb+NO+instk12/strings.xml b/libs/horizon/src/main/res/values-b+nb+NO+instk12/strings.xml index 457e73987b..9f63a57985 100644 --- a/libs/horizon/src/main/res/values-b+nb+NO+instk12/strings.xml +++ b/libs/horizon/src/main/res/values-b+nb+NO+instk12/strings.xml @@ -67,6 +67,7 @@ Oversikt Vurderinger Merknader + Verktøy Oppgavenavn – (A–Z) Forfallsdato (nyeste først) Navn: %1$s @@ -171,9 +172,10 @@ Kunne ikke oppdatere varslinger Kunne ikke laste inn varslinger %1$s sin poengsum er nÃĨ tilgjengelig - Oppgaver vurdert - Forfallsdato endret - Vurderingsvekting endret + Poengsum endret + Forfallsdato + Resultat + Beskjed Beskjed fra %1$s %1$s sin vurderingsvekting ble endret %1$s forfaller %2$s @@ -346,4 +348,35 @@ Alle Del av %s Dette faget kan ikke ÃĨpnes. + Del av %s + Velkommen! Vis programmet ditt for ÃĨ melde deg pÃĨ ditt første fag. + Programmet mitt + Kunne ikke melde deg pÃĨ i faget. + Kunneikke laste inn programmer og fag. + Kunne ikke oppdatere programmer og fag. + I dag + I gÃĨr + Kunne ikke ÃĨpne varsling + Det er ingen eksterne verktøy for dette faget. + Notatbok + Varslinger + Innboks + Fagkort %1$d av %2$d + %1$d\%% fullført + Vi kunne ikke laste inn dette innholdet. + Prøv pÃĨ nytt. + Forsøk igjen + Velkommen! Vis programmet ditt for ÃĨ melde deg pÃĨ ditt første fag. + Programdetaljer + Gratulerer! Du har fullført faget ditt. Se fremgangen og poengsummene dine pÃĨ LÃĻr-siden. + Velg fag + Lukk + Utvidet + Skjult + Utvid + Skjul + Fullført + Ikke fullført + LÃĨst + Ikke valgt diff --git a/libs/horizon/src/main/res/values-b+sv+SE+instk12/strings.xml b/libs/horizon/src/main/res/values-b+sv+SE+instk12/strings.xml index d4604bc2dc..0fb3fa5c3e 100644 --- a/libs/horizon/src/main/res/values-b+sv+SE+instk12/strings.xml +++ b/libs/horizon/src/main/res/values-b+sv+SE+instk12/strings.xml @@ -67,6 +67,7 @@ Översikt Resultat: Anteckningar + Verktyg Uppgiftsnamn (A–Z) Inlämningsdatum (senaste fÃļrst) Namn: %1$s @@ -171,9 +172,10 @@ Det gick inte att uppdatera aviseringar Det gick inte att läsa in aviseringar Resultatet fÃļr %1$s finns nu tillgängligt - Uppgiften bedÃļmd - Inlämningsdatumet ändrades - Resultatvikten har ändrats + Resultatet har ändrats + Inlämningsdatum + Resultat + Meddelande Meddelande frÃĨn %1$s Resultatvikten fÃļr %1$s har ändrats %1$s ska lämnas in %2$s @@ -346,4 +348,35 @@ Alla Del av %s Det gÃĨr inte att Ãļppna kursen. + Del av %s + Välkommen! Visa ditt program fÃļr att registrera dig i din fÃļrsta kurs. + Mitt program + Det gick inte att registrera dig i en kurs. + Det gick inte att läsa in program och kurser. + Det gick inte att uppdatera program och kurser. + I dag + I gÃĨr + Det gick inte att Ãļppna aviseringen + Det finns inga externa verktyg fÃļr den här kursen. + Anteckningsbok + Aviseringar + Inkorg + Kurskort %1$d av %2$d + %1$d\%% slutfÃļrd + Det gick inte att läsa in detta innehÃĨll. + FÃļrsÃļk igen. + FÃļrsÃļk igen + Välkommen! Visa ditt program fÃļr att registrera dig i din fÃļrsta kurs. + Programinformation + Grattis! Du har slutfÃļrt din kurs. Visa dina framsteg och poäng pÃĨ sidan Lär. + Välj kurs + Stäng + FÃļrstorad + FÃļrminskad + Expandera + Stäng + SlutfÃļrd + Inte slutfÃļrd + LÃĨst + Omarkerade diff --git a/libs/horizon/src/main/res/values-b+zh+HK/strings.xml b/libs/horizon/src/main/res/values-b+zh+HK/strings.xml index fb34b2b7bf..0473892cef 100644 --- a/libs/horizon/src/main/res/values-b+zh+HK/strings.xml +++ b/libs/horizon/src/main/res/values-b+zh+HK/strings.xml @@ -66,6 +66,7 @@ æĻ‚čĻŊ 分數 č¨ģ釋 + åˇĨå…ˇ äŊœæĨ­åį¨ą (A-Z) æˆĒæ­ĸæ—Ĩ期īŧˆæœ€æ–°å„Ē先īŧ‰ åį¨ąīŧš%1$s @@ -167,9 +168,10 @@ į„Ąæŗ•é‡æ–°æ•´į†é€šįŸĨ į„Ąæŗ•čŧ‰å…Ĩ通įŸĨ %1$s\ įš„åˆ†æ•¸įžåˇ˛æäž› - äŊœæĨ­åˇ˛čŠ•åˆ† - æˆĒæ­ĸæ—ĨæœŸåˇ˛čŽŠæ›´ - 分數äŊ”åˆ†åˇ˛čŽŠæ›´ + åˆ†æ•¸åˇ˛čŽŠæ›´ + æˆĒæ­ĸæ—Ĩ期 + 分數 + 通告 來č‡Ē %1$s įš„é€šå‘Š %1$s\ įš„åˆ†æ•¸åŠ æŦŠåˇ˛čŽŠæ›´ %1$s 在 %2$s 到期 @@ -340,4 +342,35 @@ 全部 部äģŊ %s į„Ąæŗ•é–‹å•ŸčŠ˛čĒ˛į¨‹ã€‚ + 部äģŊ %s + æ­ĄčŋŽīŧæĒĸčĻ–æ‚¨įš„æ–šæĄˆä¸Ļč¨ģå†Šæ‚¨įš„įŦŦ一門čĒ˛į¨‹ã€‚ + æˆ‘įš„æ–šæĄˆ + į„Ąæŗ•č¨ģ冊čĒ˛į¨‹ã€‚ + į„Ąæŗ•čŧ‰å…Ĩæ–šæĄˆå’ŒčĒ˛į¨‹ã€‚ + į„Ąæŗ•é‡æ–°æ•´į†æ–šæĄˆå’ŒčĒ˛į¨‹ã€‚ + äģŠå¤Š + 昨夊 + į„Ąæŗ•é–‹å•Ÿé€šįŸĨ + æœŦčĒ˛į¨‹æ˛’æœ‰å¤–éƒ¨åˇĨå…ˇã€‚ + į­†č¨˜æœŦ + 通įŸĨ + æ”ļäģļ匪 + %2$d įš„čĒ˛į¨‹åĄ %1$d + 厌成 %1$d\%% + æˆ‘å€‘į„Ąæŗ•čŧ‰å…Ĩ此內厚。 + čĢ‹é‡čŠĻ。 + 重čŠĻ + æ­ĄčŋŽīŧæĒĸčĻ–æ‚¨įš„æ–šæĄˆä¸Ļč¨ģå†Šæ‚¨įš„įŦŦ一門čĒ˛į¨‹ã€‚ + æ–šæĄˆčŠŗį´°čŗ‡æ–™ + 恭喜īŧæ‚¨åˇ˛åŽŒæˆčĒ˛į¨‹ã€‚åœ¨ã€Œå­¸įŋ’」頁éĸ上æĒĸčĻ–æ‚¨įš„é€˛åēĻ和分數。 + 選擇čǞፋ + 關閉 + åˇ˛åą•é–‹ + 厞æ”ļčĩˇ + åą•é–‹ + æ”ļčĩˇ + åˇ˛åŽŒæˆ + æœĒ厌成 + åˇ˛éŽ–åŽš + æœĒ選擇 diff --git a/libs/horizon/src/main/res/values-b+zh+Hans/strings.xml b/libs/horizon/src/main/res/values-b+zh+Hans/strings.xml index d53ac1b060..8c70a7e1a4 100644 --- a/libs/horizon/src/main/res/values-b+zh+Hans/strings.xml +++ b/libs/horizon/src/main/res/values-b+zh+Hans/strings.xml @@ -66,6 +66,7 @@ æ€ģ览 垗分、分数 å¤‡æŗ¨ + åˇĨå…ˇ äŊœä¸šåį§° (A-Z) æˆĒæ­ĸæ—Ĩ期īŧˆäģŽæ–°åˆ°æ—§īŧ‰ åį§°īŧš%1$s @@ -167,9 +168,10 @@ åˆˇæ–°é€šįŸĨå¤ąč´Ĩ 加čŊŊ通įŸĨå¤ąč´Ĩ įŽ°åœ¨å¯äģĨæŸĨįœ‹ %1$s įš„č¯„åˆ† - äŊœä¸šåˇ˛č¯„分 - æˆĒæ­ĸæ—ĨæœŸåˇ˛æ›´æ”š - č¯„åˆ†æƒé‡åˇ˛æ›´æ”š + åˆ†æ•°åˇ˛æ›´æ”š + æˆĒæ­ĸæ—Ĩ期 + č¯„åˆ† + å…Ŧ告 æĨč‡Ē %1$s įš„å…Ŧ告 %1$s įš„č¯„åˆ†æƒé‡åˇ˛æ›´æ”š %1$s įš„æˆĒæ­ĸæ—Ĩ期ä¸ē %2$s @@ -340,4 +342,35 @@ 全部 %s įš„ä¸€éƒ¨åˆ† æ— æŗ•æ‰“åŧ€č¯žį¨‹ã€‚ + %s įš„ä¸€éƒ¨åˆ† + æŦĸčŋŽīŧæŸĨįœ‹æ‚¨įš„čŽĄåˆ’äģĨæŗ¨å†ŒįŦŦ一ä¸Ēč¯žį¨‹ã€‚ + æˆ‘įš„čŽĄåˆ’ + æŗ¨å†Œč¯žį¨‹å¤ąč´Ĩ。 + 加čŊŊčŽĄåˆ’å’Œč¯žį¨‹å¤ąč´Ĩ。 + åˆˇæ–°čŽĄåˆ’å’Œč¯žį¨‹å¤ąč´Ĩ。 + äģŠå¤Š + 昨夊 + æ— æŗ•æ‰“åŧ€é€šįŸĨ + æ˛Ąæœ‰į”¨äēŽæ­¤č¯žį¨‹įš„外部åˇĨå…ˇã€‚ + įŦ”čްæœŦ + 通įŸĨ + æ”ļäģļįŽą + č¯žį¨‹åĄį‰‡ %1$d / %2$d + %1$d\%% 厌成 + æ— æŗ•åŠ čŊŊ此内厚。 + č¯ˇé‡č¯•ã€‚ + é‡č¯• + æŦĸčŋŽīŧæŸĨįœ‹æ‚¨įš„čŽĄåˆ’äģĨæŗ¨å†ŒįŦŦ一ä¸Ēč¯žį¨‹ + čŽĄåˆ’č¯Ļ情 + įĨč´ēīŧæ‚¨åˇ˛åŽŒæˆč¯žį¨‹ã€‚č¯ˇåœ¨å­Ļäš éĄĩéĸ上æŸĨįœ‹čŋ›åēĻå’Œč¯„åˆ†ã€‚ + é€‰æ‹Šč¯žį¨‹ + å…ŗé—­ + åˇ˛æ‰Šåą• + 折叠 + åą•åŧ€ + 折叠 + åˇ˛åŽŒæˆ + æœĒ厌成 + åˇ˛é”åŽš + æœĒ选中 diff --git a/libs/horizon/src/main/res/values-b+zh+Hant/strings.xml b/libs/horizon/src/main/res/values-b+zh+Hant/strings.xml index fb34b2b7bf..0473892cef 100644 --- a/libs/horizon/src/main/res/values-b+zh+Hant/strings.xml +++ b/libs/horizon/src/main/res/values-b+zh+Hant/strings.xml @@ -66,6 +66,7 @@ æĻ‚čĻŊ 分數 č¨ģ釋 + åˇĨå…ˇ äŊœæĨ­åį¨ą (A-Z) æˆĒæ­ĸæ—Ĩ期īŧˆæœ€æ–°å„Ē先īŧ‰ åį¨ąīŧš%1$s @@ -167,9 +168,10 @@ į„Ąæŗ•é‡æ–°æ•´į†é€šįŸĨ į„Ąæŗ•čŧ‰å…Ĩ通įŸĨ %1$s\ įš„åˆ†æ•¸įžåˇ˛æäž› - äŊœæĨ­åˇ˛čŠ•åˆ† - æˆĒæ­ĸæ—ĨæœŸåˇ˛čŽŠæ›´ - 分數äŊ”åˆ†åˇ˛čŽŠæ›´ + åˆ†æ•¸åˇ˛čŽŠæ›´ + æˆĒæ­ĸæ—Ĩ期 + 分數 + 通告 來č‡Ē %1$s įš„é€šå‘Š %1$s\ įš„åˆ†æ•¸åŠ æŦŠåˇ˛čŽŠæ›´ %1$s 在 %2$s 到期 @@ -340,4 +342,35 @@ 全部 部äģŊ %s į„Ąæŗ•é–‹å•ŸčŠ˛čĒ˛į¨‹ã€‚ + 部äģŊ %s + æ­ĄčŋŽīŧæĒĸčĻ–æ‚¨įš„æ–šæĄˆä¸Ļč¨ģå†Šæ‚¨įš„įŦŦ一門čĒ˛į¨‹ã€‚ + æˆ‘įš„æ–šæĄˆ + į„Ąæŗ•č¨ģ冊čĒ˛į¨‹ã€‚ + į„Ąæŗ•čŧ‰å…Ĩæ–šæĄˆå’ŒčĒ˛į¨‹ã€‚ + į„Ąæŗ•é‡æ–°æ•´į†æ–šæĄˆå’ŒčĒ˛į¨‹ã€‚ + äģŠå¤Š + 昨夊 + į„Ąæŗ•é–‹å•Ÿé€šįŸĨ + æœŦčĒ˛į¨‹æ˛’æœ‰å¤–éƒ¨åˇĨå…ˇã€‚ + į­†č¨˜æœŦ + 通įŸĨ + æ”ļäģļ匪 + %2$d įš„čĒ˛į¨‹åĄ %1$d + 厌成 %1$d\%% + æˆ‘å€‘į„Ąæŗ•čŧ‰å…Ĩ此內厚。 + čĢ‹é‡čŠĻ。 + 重čŠĻ + æ­ĄčŋŽīŧæĒĸčĻ–æ‚¨įš„æ–šæĄˆä¸Ļč¨ģå†Šæ‚¨įš„įŦŦ一門čĒ˛į¨‹ã€‚ + æ–šæĄˆčŠŗį´°čŗ‡æ–™ + 恭喜īŧæ‚¨åˇ˛åŽŒæˆčĒ˛į¨‹ã€‚åœ¨ã€Œå­¸įŋ’」頁éĸ上æĒĸčĻ–æ‚¨įš„é€˛åēĻ和分數。 + 選擇čǞፋ + 關閉 + åˇ˛åą•é–‹ + 厞æ”ļčĩˇ + åą•é–‹ + æ”ļčĩˇ + åˇ˛åŽŒæˆ + æœĒ厌成 + åˇ˛éŽ–åŽš + æœĒ選擇 diff --git a/libs/horizon/src/main/res/values-ca/strings.xml b/libs/horizon/src/main/res/values-ca/strings.xml index 6cd874293d..c621b27d6e 100644 --- a/libs/horizon/src/main/res/values-ca/strings.xml +++ b/libs/horizon/src/main/res/values-ca/strings.xml @@ -67,6 +67,7 @@ Vista general Puntuacions Comentaris + Eines Nom de l\'activitat (A-Z) Data de lliurament (la mÊs recent primer) Nom: %1$s @@ -171,9 +172,10 @@ No s\'han pogut actualitzar les notificacions No s\'han pogut carregar les notificacions En aquest moment, està disponible la puntuaciÃŗ de %1$s - S’ha puntuat l\'activitat - Ha canviat la data de lliurament - Ha canviat la ponderaciÃŗ de la puntuaciÃŗ + La puntuaciÃŗ ha canviat + Data de lliurament + PuntuaciÃŗ + Anunci Anunci de %1$s Ha canviat la ponderaciÃŗ de la puntuaciÃŗ de %1$s %1$s venç el %2$s @@ -346,4 +348,35 @@ Tot Part de %s No es pot obrir l’assignatura. + Part de %s + Us donem la benvinguda! Consulteu el programa per inscriure-us en la primera assignatura. + El meu programa + No s\'ha pogut inscriure en l’assignatura. + No s\'han pogut carregar els programes ni les assignatures. + No s\'han pogut actualitzar els programes ni les assignatures. + Avui + Ahir + No es pot obrir la notificaciÃŗ + No hi ha cap eina externa per a aquesta assignatura. + Llibreta + Notificacions + Safata d\'entrada + Targeta de l’assignatura %1$d de %2$d + %1$d % complet + No hem pogut carregar aquest contingut. + Torneu a provar-ho. + Torna a provar-ho + Us donem la benvinguda! Consulteu el programa per inscriure-us en la primera assignatura. + InformaciÃŗ del programa + Enhorabona! Heu completat l’assignatura. Consulteu el vostre progrÊs i les puntuacions a la pàgina Aprèn. + Seleccioneu l\'assignatura + Tanca-ho + S’ha desplegat + S’ha contret + Desplega-ho + Contrau-ho + S’ha completat + No s\'ha completat + S’ha bloquejat + S’ha n’anul¡lat la selecciÃŗ diff --git a/libs/horizon/src/main/res/values-cy/strings.xml b/libs/horizon/src/main/res/values-cy/strings.xml index f508098de1..4f1242b2d1 100644 --- a/libs/horizon/src/main/res/values-cy/strings.xml +++ b/libs/horizon/src/main/res/values-cy/strings.xml @@ -67,6 +67,7 @@ Trosolwg Sgorau Nodiadau + Adnoddau Enw\'r aseiniad (A-Z) Dyddiad erbyn (diweddaraf yn gyntaf) Enw: %1$s @@ -171,9 +172,10 @@ Wedi methu adnewyddu’r hysbysiadau Wedi methu llwytho hysbysiadau Mae sgôr %1$s ar gael nawr - Rhoddwyd sgôr i aseiniad - Dyddiad erbyn wedi newid - Pwysoliad sgorio wedi newid + Sgôr wedi newid + Dyddiad erbyn + Sgôr + Cyhoeddiad Cyhoeddiad gan %1$s Newidiwyd pwysiad sgôr %1$s Mae angen cyflwyno %1$s ar %2$s @@ -346,4 +348,35 @@ Y cyfan Rhan o %s Does dim modd agor y cwrs. + Rhan o %s + Croeso! Edrychwch ar eich rhaglen i gofrestru ar gyfer eich cwrs cyntaf. + Fy rhaglen + Wedi methu cofrestru ar gyfer y cwrs. + Wedi methu lwytho rhaglenni a chyrsiau. + Wedi methu adnewyddu rhaglenni a chyrsiau. + Heddiw + Ddoe + Methu agor yr Hysbysiad + Does dim adnoddau allanol ar gyfer y cwrs hwn. + Llyfr nodiadau + Hysbysiadau + Blwch Derbyn + Cerdyn Cwrs %1$d o %2$d + %1$d\%% wedi cwblhau + Wedi methu llwytho’r cynnwys hwn. + Rhowch gynnig arall arni. + Ailgynnig + Croeso! Edrychwch ar eich rhaglen i gofrestru ar gyfer eich cwrs cyntaf. + Manylion y rhaglen + Llongyfarchiadau! Rydych chi wedi cwblhau eich cwrs. Edrychwch ar eich cynnydd a’ch sgoriau ar y dudalen Dysgu. + Dewis Cwrs + Cau + Wedi ehangu + Wedi crebachu + Ehangu + Crebachu + Wedi cwblhau + Heb gwblhau + Wedi cloi + Heb ddewis diff --git a/libs/horizon/src/main/res/values-da/strings.xml b/libs/horizon/src/main/res/values-da/strings.xml index 324595e1ca..a4215b1c38 100644 --- a/libs/horizon/src/main/res/values-da/strings.xml +++ b/libs/horizon/src/main/res/values-da/strings.xml @@ -67,6 +67,7 @@ Oversigt Resultater Noter + VÃĻrktøjer Opgavenavn (A-Å) Forfaldsdato (nyeste først) Navn: %1$s @@ -171,9 +172,10 @@ Meddelelser kunne ikke opdateres Meddelelser kunne ikke indlÃĻses %1$s\s resultat er nu tilgÃĻngeligt - Opgavens resultat - Afleveringsdato ÃĻndret - Resultatets vÃĻgtning er blevet ÃĻndret + Resultatet blev ÃĻndret + Afleveringsdato + Resultat + Meddelelse Meddelelse fra %1$s VÃĻgtning af %1$s\s resultat blev ÃĻndret %1$s skal afleveres før den %2$s @@ -346,4 +348,35 @@ Alle En del af %s Faget kan ikke ÃĨbnes. + En del af %s + Velkommen! Se dit program for at tilmelde dig dit første fag. + Mit program + Kunne ikke tilmelde til faget. + Kunne ikke indlÃĻse programmer og fag. + Kunne ikke opdatere programmer og fag. + I dag + I gÃĨr + Kan ikke ÃĨbne meddelelsen + Der findes ingen eksterne vÃĻrktøjer til dette fag. + Notesbog + Notifikationer + Indbakke + Fagkort %1$d af %2$d + %1$d\%% afsluttet + Vi kunne ikke indlÃĻse dette indhold. + Prøv igen. + Prøv igen + Velkommen! Se dit program for at tilmelde dig dit første fag. + Programdetaljer + Tillykke! Du har afsluttet dit fag. Se dine fremskridt og resultater pÃĨ siden LÃĻr. + VÃĻlg fag + Luk + Udvidet + Skjult + Udvid + Skjul + Fuldført + Ikke afsluttet + LÃĨst + Ikke valgt diff --git a/libs/horizon/src/main/res/values-de/strings.xml b/libs/horizon/src/main/res/values-de/strings.xml index c4e1c53724..488b0df1e2 100644 --- a/libs/horizon/src/main/res/values-de/strings.xml +++ b/libs/horizon/src/main/res/values-de/strings.xml @@ -67,6 +67,7 @@ Überblick Punktzahlen Anmerkungen + Tools Name der Aufgabe (A-Z) Fälligkeitsdatum (aktuellstes zuerst) Name: %1$s @@ -171,9 +172,10 @@ Benachrichtigungen konnten nicht neu geladen werden Benachrichtigungen konnten nicht geladen werden Ergebnis von %1$s ist jetzt verfÃŧgbar - Aufgabe bewertet - Fälligkeitsdatum geändert - Gewichtung der Bewertung geändert + Punktzahl geändert + Fälligkeitsdatum + Punktestand + AnkÃŧndigung AnkÃŧndigung von %1$s Punktegewichtung fÃŧr %1$s wurde geändert %1$s ist fällig am %2$s @@ -346,4 +348,35 @@ Alle Teil von %s Der Kurs kann nicht geÃļffnet werden. + Teil von %s + Willkommen! Sehen Sie sich Ihr Programm an, um sich fÃŧr Ihren ersten Kurs einzuschreiben. + Mein Programm + Kurseinschreibung konnte nicht durchgefÃŧhrt werden. + Programme und Kurse konnten nicht geladen werden. + Programme und Kurse konnten nicht aktualisiert werden. + Heute + Gestern + Benachrichtigung kann nicht geÃļffnet werden + Es gibt keine externen Tools fÃŧr diesen Kurs. + Notizbuch + Benachrichtigungen + Posteingang + Kurskarte %1$d von %2$d + %1$d\%% vollständig + Wir kÃļnnen diesen Inhalt nicht laden. + Bitte versuchen Sie es erneut. + Erneut versuchen + Willkommen! Sehen Sie sich Ihr Programm an, um sich fÃŧr Ihren ersten Kurs einzuschreiben. + Programmdetails + GlÃŧckwunsch! Sie haben den Kurs abgeschlossen. Sehen Sie sich Ihren Fortschritt und Ihre Punktzahl auf der Seite „Lernen“ an. + Kurs auswählen + Schließen + Erweitert + Reduziert + Erweitern + Ausblenden + Fertiggestellt + Nicht abgeschlossen + Gesperrt + Nicht ausgewählt diff --git a/libs/horizon/src/main/res/values-en-rAU/strings.xml b/libs/horizon/src/main/res/values-en-rAU/strings.xml index 57f34679fb..d62ad54c51 100644 --- a/libs/horizon/src/main/res/values-en-rAU/strings.xml +++ b/libs/horizon/src/main/res/values-en-rAU/strings.xml @@ -67,6 +67,7 @@ Overview Scores Notes + Tools Assignment name (A-Z) Due date (newest first) Name: %1$s @@ -171,9 +172,10 @@ Failed to refresh notifications Failed to load notifications %1$s\'s score is now available - Assignment scored - Due date changed - Scoring weight changed + Score changed + Due date + Score + Announcement Announcement from %1$s %1$s\'s score weight was changed %1$s is due on %2$s @@ -346,4 +348,35 @@ All Part of %s The course cannot be opened. + Part of %s + Welcome! View your program to enrol in your first course. + My program + Failed to enrol in course. + Failed to load programs and courses. + Failed to refresh programs and courses. + Today + Yesterday + Unable to open Notification + There are no external tools for this course. + Notebook + Notifications + Inbox + Course Card %1$d of %2$d + %1$d\%% complete + We weren’t able to load this content. + Please try again. + Retry + Welcome! View your program to enrol in your first course. + Program details + Congrats! You’ve completed your course. View your progress and scores on the Learn page. + Select Course + Close + Expanded + Collapsed + Expand + Collapse + Completed + Not completed + Locked + Unselected diff --git a/libs/horizon/src/main/res/values-en-rCY/strings.xml b/libs/horizon/src/main/res/values-en-rCY/strings.xml index 91f6bd4224..ff4565981d 100644 --- a/libs/horizon/src/main/res/values-en-rCY/strings.xml +++ b/libs/horizon/src/main/res/values-en-rCY/strings.xml @@ -67,6 +67,7 @@ Overview Scores Notes + Tools Assignment name (A-Z) Due date (newest first) Name: %1$s @@ -171,9 +172,10 @@ Failed to refresh notifications Failed to load notifications %1$s\'s score is now available - Assignment scored - Due date changed - Scoring weight changed + Score changed + Due date + Score + Announcement Announcement from %1$s %1$s\'s score weight was changed %1$s is due on %2$s @@ -346,4 +348,35 @@ All Part of %s The module cannot be opened. + Part of %s + Welcome! View your program to enrol in your first module. + My program + Failed to enrol in module. + Failed to load programs and modules. + Failed to refresh programs and modules. + Today + Yesterday + Unable to open Notification + There are no external tools for this module. + Notebook + Notifications + Inbox + Module Card %1$d of %2$d + %1$d\%% complete + We weren’t able to load this content. + Please try again. + Retry + Welcome! View your program to enrol in your first module. + Program details + Congrats! You’ve completed your module. View your progress and scores on the Learn page. + Select Module + Close + Expanded + Collapsed + Expand + Collapse + Completed + Not completed + Locked + Unselected diff --git a/libs/horizon/src/main/res/values-en-rGB/strings.xml b/libs/horizon/src/main/res/values-en-rGB/strings.xml index 40cfcbc0b7..b726e0bd60 100644 --- a/libs/horizon/src/main/res/values-en-rGB/strings.xml +++ b/libs/horizon/src/main/res/values-en-rGB/strings.xml @@ -67,6 +67,7 @@ Overview Scores Notes + Tools Assignment name (A-Z) Due date (newest first) Name: %1$s @@ -171,9 +172,10 @@ Failed to refresh notifications Failed to load notifications %1$s\'s score is now available - Assignment scored - Due date changed - Scoring weight changed + Score changed + Due date + Score + Announcement Announcement from %1$s %1$s\'s score weight was changed %1$s is due on %2$s @@ -346,4 +348,35 @@ All Part of %s The course cannot be opened. + Part of %s + Welcome! View your program to enrol in your first course. + My program + Failed to enrol in course. + Failed to load programs and courses. + Failed to refresh programs and courses. + Today + Yesterday + Unable to open Notification + There are no external tools for this course. + Notebook + Notifications + Inbox + Course Card %1$d of %2$d + %1$d\%% complete + We weren’t able to load this content. + Please try again. + Retry + Welcome! View your program to enrol in your first course. + Program details + Congrats! You’ve completed your course. View your progress and scores on the Learn page. + Select Course + Close + Expanded + Collapsed + Expand + Collapse + Completed + Not completed + Locked + Unselected diff --git a/libs/horizon/src/main/res/values-en/strings.xml b/libs/horizon/src/main/res/values-en/strings.xml index 7194792ee1..16936cde6f 100644 --- a/libs/horizon/src/main/res/values-en/strings.xml +++ b/libs/horizon/src/main/res/values-en/strings.xml @@ -66,6 +66,7 @@ Overview Scores Notes + Tools Assignment name (A-Z) Due date (newest first) Name: %1$s @@ -170,9 +171,10 @@ Failed to refresh notifications Failed to load notifications %1$s\'s score is now available - Assignment scored - Due date changed - Scoring weight changed + Score changed + Due date + Score + Announcement Announcement from %1$s %1$s\'s score weight was changed %1$s is due on %2$s @@ -345,4 +347,35 @@ All Part of %s The course cannot be opened. + Part of %s + Welcome! View your program to enroll in your first course. + My program + Failed to enroll in course. + Failed to load programs and courses. + Failed to refresh programs and courses. + Today + Yesterday + Unable to open Notification + There are no external tools for this course. + Notebook + Notifications + Inbox + Course Card %1$d of %2$d + %1$d\%% complete + We weren’t able to load this content. + Please try again. + Retry + Welcome! View your program to enroll in your first course. + Program details + Congrats! You’ve completed your course. View your progress and scores on the Learn page. + Select Course + Close + Expanded + Collapsed + Expand + Collapse + Completed + Not completed + Locked + Unselected \ No newline at end of file diff --git a/libs/horizon/src/main/res/values-es-rES/strings.xml b/libs/horizon/src/main/res/values-es-rES/strings.xml index 858bf26d47..8bdf410869 100644 --- a/libs/horizon/src/main/res/values-es-rES/strings.xml +++ b/libs/horizon/src/main/res/values-es-rES/strings.xml @@ -67,6 +67,7 @@ Resumen PuntuaciÃŗn Comentarios + Herramientas Nombre de la actividad (A-Z) Fecha de entrega (primero la mÃĄs reciente) Nombre: %1$s @@ -171,9 +172,10 @@ No se han podido actualizar las notificaciones No se han podido cargar las notificaciones Ya estÃĄ disponible la puntuaciÃŗn de %1$s - Actividad calificada - Cambio de fecha de entrega - Cambio de ponderaciÃŗn de la puntuaciÃŗn + PuntuaciÃŗn cambiada + Fecha de entrega + PuntuaciÃŗn + Anuncio Anuncio de %1$s Se ha cambiado la ponderaciÃŗn de la puntuaciÃŗn de %1$s %1$s vence el %2$s @@ -346,4 +348,35 @@ Todo Parte de %s No puede abrirse la asignatura. + Parte de %s + ÂĄTe damos la bienvenida! Consulta tu programa para inscribirte en tu primera asignatura. + Mi programa + No se ha podido realizar la inscripciÃŗn en la asignatura. + No se han podido cargar los programas y asignaturas. + No se han podido actualizar los programas y asignaturas. + Hoy + Ayer + No se ha podido abrir la notificaciÃŗn + No hay ninguna herramienta externa para esta asignatura. + Cuaderno + Notificaciones + Bandeja de entrada + Tarjeta de asignatura %1$d de %2$d + %1$d\%% completado + No hemos podido cargar este contenido. + IntÊntalo de nuevo. + Reintentar + ÂĄTe damos la bienvenida! Consulta tu programa para inscribirte en tu primera asignatura. + Detalles del programa + ÂĄEnhorabuena! Has completado tu asignatura. Consulta tus progresos y puntuaciones en la pÃĄgina Aprendizaje. + Seleccionar asignatura + Cerrar + Expandido + Colapsado + Expandir + Colapsar + Completado + No completado + Bloqueado + Sin seleccionar diff --git a/libs/horizon/src/main/res/values-es/strings.xml b/libs/horizon/src/main/res/values-es/strings.xml index b4c658c3d6..c17c6f6363 100644 --- a/libs/horizon/src/main/res/values-es/strings.xml +++ b/libs/horizon/src/main/res/values-es/strings.xml @@ -67,6 +67,7 @@ DescripciÃŗn general Puntajes Notas + Herramientas Nombre de la tarea (A-Z) Fecha de entrega (de la mÃĄs reciente a la mÃĄs antigua) Nombre: %1$s @@ -171,9 +172,10 @@ No se pudieron actualizar las notificaciones No se pudieron cargar las notificaciones El puntaje de %1$s ya estÃĄ disponible - Tarea puntuada - CambiÃŗ la fecha de entrega - CambiÃŗ el peso del puntaje + Puntaje modificado + Fecha de entrega + Puntaje + Anuncio Anuncio de %1$s CambiÃŗ la ponderaciÃŗn del puntaje de %1$s %1$s tiene fecha de entrega el %2$s @@ -346,4 +348,35 @@ Todo Parte de %s No se puede abrir el curso. + Parte de %s + ÂĄBienvenido! Consulte su programa para inscribirse en el primer curso. + Mi programa + No se pudo inscribir en el curso. + No se pudieron cargar los programas ni los cursos. + No se pudieron actualizar los programas ni los cursos. + Hoy + Ayer + No se puede abrir la notificaciÃŗn + No hay herramientas externas para este curso. + Cuaderno + Notificaciones + Bandeja de entrada + Tarjeta del curso %1$d de %2$d + %1$d\%% completo + No pudimos cargar este contenido. + IntÊntelo de nuevo. + Reintentar + ÂĄBienvenido! Consulte su programa para inscribirse en el primer curso. + Detalles del programa + ÂĄFelicidades! CompletÃŗ el curso. Vea su progreso y sus puntajes en la pÃĄgina Aprender. + Seleccionar curso + Cerrar + Expandido + Colapsado + Expandir + Colapsar + Completado + Sin completar + Bloqueado + No seleccionado diff --git a/libs/horizon/src/main/res/values-fi/strings.xml b/libs/horizon/src/main/res/values-fi/strings.xml index 0bb4171319..ae7fe93cb6 100644 --- a/libs/horizon/src/main/res/values-fi/strings.xml +++ b/libs/horizon/src/main/res/values-fi/strings.xml @@ -67,6 +67,7 @@ Yleiskuva Pisteet Huomautukset + TyÃļkalut Tehtävän nimi (A-Z) Määräpäivä (uusimmasta vanhimpaan) Nimi: %1$s @@ -171,9 +172,10 @@ Ilmoitusten päivitys epäonnistui Ilmoitusten lataus epäonnistui %1$s pistemäärä on nyt saatavissa - Tehtävälle annettu pistemäärä - Eräpäivä muutettu - Pistemäärän painoarvoa muutettu + Pistemäärä muutettu + Määräpäivä + Pistemäärä + Ilmoitus Ilmoitus kohteesta %1$s %1$s pistemäärän painoarvo muuttui %1$s erääntymispäivä on %2$s @@ -346,4 +348,35 @@ Kaikki Osa kohteesta %s Kurssin avaaminen ei onnistunu. + Osa kohteesta %s + Tervetuloa! Näytä ohjelmasi ilmoittautuaksesi ensimmäiselle kurssillesi. + Oma ohjelma + Kurssille ilmoittautuminen epäonnistui. + Lukitse nopeus 1x + Ohjelmien ja kurssien lataaminen epäonnistui. + Tänään + Eilen + Ilmoitusta ei voida avata + Tällä kurssilla ei ole ulkoisia tyÃļkaluja. + Muistikirja + Ilmoitukset + Saapuvien postien laatikko + Kurssikortti %1$d/%2$d + %1$d\%% valmis + Emme voineet ladata tätä sisältÃļä. + Yritä uudelleen. + Yritä uudelleen + Tervetuloa! Näytä ohjelmasi ilmoittautuaksesi ensimmäiselle kurssillesi. + Ohjelman tiedot + Onnittelut! Olet suorittanut kurssisi. Katso edistymistäsi ja pisteitäsi Opi-sivulla. + Valitse kurssi + Sulje + Laajennettu + Kutistettu + Laajenna + Kutista + Suoritettu + Ei valmis + Lukittu + Valinta poistettu diff --git a/libs/horizon/src/main/res/values-fr-rCA/strings.xml b/libs/horizon/src/main/res/values-fr-rCA/strings.xml index 714a8dbe81..1a2191fcd3 100644 --- a/libs/horizon/src/main/res/values-fr-rCA/strings.xml +++ b/libs/horizon/src/main/res/values-fr-rCA/strings.xml @@ -67,6 +67,7 @@ Aperçu Scores Remarques + Outils Nom du travail (A-Z) Date d’ÊchÊance (la plus rÊcente en premier) Nom : %1$s @@ -171,9 +172,10 @@ Échec de l’actualisation des notifications Échec du chargement des notifications Le score de %1$s est maintenant disponible - Travail notÊ - Changement de la date d’ÊchÊance - Modification de la pondÊration de la notation + Le score a changÊ + Date d’ÊchÊance + Score + Annonce Annonce de %1$s La pondÊration du score de %1$s a ÊtÊ modifiÊ %1$s est due le %2$s @@ -346,4 +348,35 @@ Tout Partie de %s Le cours ne peut pas ÃĒtre ouvert. + Partie de %s + Bienvenue! Consultez votre programme pour vous inscrire à votre premier cours. + Mon programme + N’a pas rÊussi à s’inscrire au cours. + Échec du chargement des programmes et des cours. + N’a pas rÊussi à actualiser les programmes et les cours. + Aujourd’hui + Hier + Impossible d\'ouvrir la notification + Il n’y a pas d’outils externes pour ce cours. + Carnet de notes + Notifications + BoÃŽte de rÊception + Carte de cours %1$d de %2$d + %1$d\%% terminÊ + Nous n\'avons pas pu charger ce contenu. + Veuillez rÊessayer. + RÊessayer + Bienvenue! Consultez votre programme pour vous inscrire à votre premier cours. + DÊtails du programme + FÊlicitations! Vous avez terminÊ votre formation. Consultez vos progrès et vos scores sur la page Apprendre. + SÊlectionner un cours + Fermer + DÊveloppÊ(e) + RÊduit(e) + DÊvelopper + RÊduire + TerminÊ + Non terminÊ + VerrouillÊ + Non sÊlectionnÊ diff --git a/libs/horizon/src/main/res/values-fr/strings.xml b/libs/horizon/src/main/res/values-fr/strings.xml index 766b8b79cc..5bb1ed6f88 100644 --- a/libs/horizon/src/main/res/values-fr/strings.xml +++ b/libs/horizon/src/main/res/values-fr/strings.xml @@ -67,6 +67,7 @@ Vue d’ensemble Scores Remarques + Outils Nom du travail (A-Z) Date d’ÊchÊance (la plus rÊcente en premier) Nom : %1$s @@ -171,9 +172,10 @@ Échec d’actualisation des notifications Échec de chargement des notifications La note de %1$s est dÊsormais disponible. - Travail notÊ - Date d’ÊchÊance modifiÊe - Coefficient de note modifiÊ + Score modifiÊ + Date d’ÊchÊance + Score + Annonce Annonce de %1$s Modification du coefficient de notation de %1$s %1$s à rendre le %2$s @@ -346,4 +348,35 @@ Tout Une partie de %s Impossible d’ouvrir le cours. + Une partie de %s + Bienvenue ! Affichez votre programme pour vous inscrire à votre premier cours. + Mon programme + Échec de l’inscription au cours. + Échec du chargement des cours et programmes. + Échec de l’actualisation des cours et programmes. + Aujourd’hui + Hier + Impossible d’ouvrir la notification + Il n’existe aucun outil externe pour ce cours. + Carnet + Notifications + BoÃŽte de rÊception + Carte %1$d sur %2$d du cours + %1$d\%% terminÊ + Nous n’avons pas pu charger ce contenu. + Veuillez rÊessayer. + RÊessayer + Bienvenue ! Affichez votre programme pour vous inscrire à votre premier cours. + DÊtails du programme + FÊlicitations ! Vous avez terminÊ votre cours. Affichez vos scores et progrès sur la page Apprentissage. + SÊlectionner un cours + Fermer + DÊveloppÊ(e) + RÊduit(e) + DÊvelopper + RÊduire + TerminÊ + Non terminÊ + VerrouillÊ + Non sÊlectionnÊ diff --git a/libs/horizon/src/main/res/values-ga/strings.xml b/libs/horizon/src/main/res/values-ga/strings.xml index 269181555a..b5154b34a4 100644 --- a/libs/horizon/src/main/res/values-ga/strings.xml +++ b/libs/horizon/src/main/res/values-ga/strings.xml @@ -67,8 +67,9 @@ ForbhreathnÃē ScÃŗir nÃŗtaí mínithe - Ainm an taisc - DÃĄta dlite + Uirlisí + Ainm an taisc (A-Z) + DÃĄta dlite (an chÊad cheann is nua) Ainm: %1$s DÃĄta: %1$s No due date @@ -121,6 +122,7 @@ Casta Tacaíocht Tabhair aiseolas + Tuairiscigh fabht Pobal bÊite Ainm iomlÃĄn DÊan nascleanÃēint ar ais @@ -170,9 +172,10 @@ Theip ar na fÃŗgraí a athnuachan Theip ar fhÃŗgraí a lÃŗdÃĄil TÃĄ scÃŗr %1$s ar fÃĄil anois - ScÃŗrÃĄil an tasc - DÃĄta dlite athraithe - Athraíodh ualach scÃŗrÃĄla + ScÃŗr athraithe + DÃĄta dlite + ScÃŗr + FÃŗgra FÃŗgra Ãŗ %1$s Athraíodh meÃĄchan scÃŗr %1$s %1$s dlite ar %2$s @@ -327,7 +330,53 @@ Theip ar ghlacadh le cuireadh an chÃērsa Níl tÃē rollaithe i gcÃērsa faoi lÃĄthair. Níl tÃē rollaithe i gcÃērsa faoi lÃĄthair. - %s críochnaithe + %s\%% críochnaithe Níor tosaíodh Rollaigh + %1$s - %2$s + Ag teastÃĄil + Roghnach + Faoi ghlas + Críochnaithe + Rollaithe + + Críochnaithe %1$d as %2$d cÃērsa + Críochnaithe %1$d as %2$d cÃērsaí + + Theip ar an gclÃĄr a athnuachan. + ForbhreathnÃē ar an gclÃĄr + Gach + Cuid de %s + Ní fÊidir an cÃērsa a oscailt. + Cuid de %s + FÃĄilte romhat! FÊach ar do chlÃĄr le rollÃē i do chÊad chÃērsa. + Mo chlÃĄr + Theip ar rollÃē sa chÃērsa. + Theip ar chlÃĄir agus cÃērsaí a lÃŗdÃĄil. + Theip ar na clÃĄir agus na cÃērsaí a athnuachan. + Today + InnÊ + Ní fÊidir an fÃŗgra a oscailt + Níl aon uirlis sheachtrach ann don chÃērsa seo. + Leabhar nÃŗtaí + FÃŗgraí + Bosca Isteach + CÃĄrta CÃērsa %1$d de %2$d + %1$d\%% críochnaithe + Ní raibh muid in ann an t-ÃĄbhar seo a lÃŗdÃĄil. + Bain triail as arís, le do thoil. + Bain triail eile as + FÃĄilte romhat! FÊach ar do chlÃĄr chun clÃĄrÃē i do chÊad chÃērsa. + Sonraí an chlÃĄir + Comhghairdeas! TÃĄ do chÃērsa críochnaithe agat. FÊach ar do dhul chun cinn agus do scÃŗir ar an leathanach Foghlama. + Roghnaigh CÃērsa + DÃēn + Leathnaithe + Leacaigh + Leathnaigh + Leacaigh + Críochnaithe + Níl sÊ críochnaithe + Faoi ghlas + Gan roghnÃē diff --git a/libs/horizon/src/main/res/values-hi/strings.xml b/libs/horizon/src/main/res/values-hi/strings.xml index 25e7f60163..4371ea3140 100644 --- a/libs/horizon/src/main/res/values-hi/strings.xml +++ b/libs/horizon/src/main/res/values-hi/strings.xml @@ -67,6 +67,7 @@ ⤅ā¤ĩ⤞āĨ‹ā¤•⤍ ⤅⤂⤕ ⤍āĨ‹ā¤ŸāĨā¤¸ + ⤉ā¤Ē⤕⤰⤪ ā¤…ā¤¸ā¤žā¤‡ā¤¨ā¤ŽāĨ‡ā¤‚ā¤Ÿ ā¤¨ā¤žā¤Ž (A-Z) ⤍ā¤ŋ⤝⤤ ⤤ā¤ŋā¤Ĩā¤ŋ (⤍ā¤ĩāĨ€ā¤¨ā¤¤ā¤Ž ⤏ā¤Ŧ⤏āĨ‡ ā¤Ēā¤šā¤˛āĨ‡) ā¤¨ā¤žā¤Ž: %1$s @@ -171,9 +172,10 @@ ⤏āĨ‚ā¤šā¤¨ā¤žā¤ā¤‚ ⤰ā¤ŋāĨžāĨā¤°āĨ‡ā¤ļ ⤕⤰⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤ĩā¤ŋā¤Ģ⤞ ⤏āĨ‚ā¤šā¤¨ā¤žā¤ā¤‚ ⤞āĨ‹ā¤Ą ⤕⤰⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤ĩā¤ŋā¤Ģ⤞ %1$sā¤•ā¤ž ⤅⤂⤕ ⤅ā¤Ŧ ⤉ā¤Ē⤞ā¤ŦāĨā¤§ ā¤šāĨˆ - ā¤…ā¤¸ā¤žā¤‡ā¤¨ā¤ŽāĨ‡ā¤‚ā¤Ÿ ⤕āĨ‹ ⤅⤂⤕ ā¤Ļā¤ŋā¤ ā¤—ā¤ - ⤍ā¤ŋ⤝⤤ ⤤ā¤ŋā¤Ĩā¤ŋ ā¤Ŧā¤Ļ⤞āĨ€ ā¤—ā¤ˆ - ⤅⤂⤕ ā¤­ā¤žā¤° ā¤Ŧā¤Ļā¤˛ā¤ž ā¤—ā¤¯ā¤ž + ⤅⤂⤕ ā¤Ŧā¤Ļ⤞ ā¤—ā¤¯ā¤ž + ⤍ā¤ŋ⤝⤤ ⤤ā¤ŋā¤Ĩā¤ŋ + ⤅⤂⤕ + ⤘āĨ‹ā¤ˇā¤Ŗā¤ž %1$s ⤏āĨ‡ ⤘āĨ‹ā¤ˇā¤Ŗā¤ž %1$s ā¤•ā¤ž ⤅⤂⤕ ā¤­ā¤žā¤° ā¤Ŧā¤Ļā¤˛ā¤ž ā¤—ā¤¯ā¤ž %1$s %2$s ⤕āĨ‹ ⤍ā¤ŋ⤝⤤ ā¤šāĨˆ @@ -346,4 +348,35 @@ ⤏⤭āĨ€ %s ā¤•ā¤ž ā¤šā¤ŋ⤏āĨā¤¸ā¤ž ā¤Ēā¤žā¤ āĨā¤¯ā¤•āĨā¤°ā¤Ž ā¤¨ā¤šāĨ€ā¤‚ ⤖āĨ‹ā¤˛ā¤ž ā¤œā¤ž ā¤¸ā¤•ā¤¤ā¤žāĨ¤ + %s ā¤•ā¤ž ā¤šā¤ŋ⤏āĨā¤¸ā¤ž + ⤆ā¤Ēā¤•ā¤ž ⤏āĨā¤ĩā¤žā¤—ā¤¤ ā¤šāĨˆ! ⤅ā¤Ē⤍āĨ‡ ā¤Ēā¤šā¤˛āĨ‡ ā¤Ēā¤žā¤ āĨā¤¯ā¤•āĨā¤°ā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤¨ā¤žā¤Žā¤žā¤‚ā¤•ā¤¨ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤅ā¤Ēā¤¨ā¤ž ā¤•ā¤žā¤°āĨā¤¯ā¤•āĨā¤°ā¤Ž ā¤ĻāĨ‡ā¤–āĨ‡ā¤‚āĨ¤ + ā¤ŽāĨ‡ā¤°ā¤ž ā¤•ā¤žā¤°āĨā¤¯ā¤•āĨā¤°ā¤Ž + ā¤Ēā¤žā¤ āĨā¤¯ā¤•āĨā¤°ā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤¨ā¤žā¤Žā¤žā¤‚ā¤•ā¤¨ ⤕⤰⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤ĩā¤ŋā¤Ģ⤞āĨ¤ + ā¤•ā¤žā¤°āĨā¤¯ā¤•āĨā¤°ā¤Ž ⤔⤰ ā¤Ēā¤žā¤ āĨā¤¯ā¤•āĨā¤°ā¤Ž ⤞āĨ‹ā¤Ą ⤕⤰⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤ĩā¤ŋā¤Ģ⤞āĨ¤ + ā¤ĒāĨā¤°āĨ‹ā¤—āĨā¤°ā¤žā¤Ž ⤔⤰ ā¤Ēā¤žā¤ āĨā¤¯ā¤•āĨā¤°ā¤Ž ⤰āĨ€ā¤Ģā¤ŧāĨā¤°āĨ‡ā¤ļ ⤕⤰⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤ĩā¤ŋā¤Ģ⤞āĨ¤ + ā¤†ā¤œ + ā¤ŦāĨ€ā¤¤ā¤ž ⤕⤞ + ⤏āĨ‚ā¤šā¤¨ā¤žā¤ā¤‚ ⤖āĨ‹ā¤˛ā¤¨āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ + ⤇⤏ ā¤Ēā¤žā¤ āĨā¤¯ā¤•āĨā¤°ā¤Ž ⤕āĨ‡ ⤞ā¤ŋā¤ ⤕āĨ‹ā¤ˆ ā¤Ŧā¤žā¤šā¤°āĨ€ ⤉ā¤Ē⤕⤰⤪ ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆā¤‚āĨ¤ + ⤍āĨ‹ā¤Ÿā¤ŦāĨā¤• + ⤏āĨ‚ā¤šā¤¨ā¤žā¤ā¤‚ + ⤇⤍ā¤ŦāĨ‰ā¤•āĨā¤¸ + ā¤Ēā¤žā¤ āĨā¤¯ā¤•āĨā¤°ā¤Ž ā¤•ā¤žā¤°āĨā¤Ą %2$d ā¤ŽāĨ‡ā¤‚ ⤏āĨ‡ %1$d + %1$d\%% ā¤ĒāĨ‚ā¤°ā¤ž ā¤šāĨā¤† + ā¤šā¤Ž ā¤¯ā¤š ā¤¸ā¤žā¤Žā¤—āĨā¤°āĨ€ ⤞āĨ‹ā¤Ą ā¤¨ā¤šāĨ€ā¤‚ ⤕⤰ ā¤Ēā¤žā¤āĨ¤ + ⤕āĨƒā¤Ēā¤¯ā¤ž ā¤Ģā¤ŋ⤰ ⤏āĨ‡ ⤕āĨ‹ā¤ļā¤ŋā¤ļ ⤕⤰āĨ‡ā¤‚āĨ¤ + ā¤Ģā¤ŋ⤰ ⤏āĨ‡ ⤕āĨ‹ā¤ļā¤ŋā¤ļ ⤕⤰āĨ‡ā¤‚ + ⤆ā¤Ēā¤•ā¤ž ⤏āĨā¤ĩā¤žā¤—ā¤¤ ā¤šāĨˆ! ⤅ā¤Ē⤍āĨ‡ ā¤Ēā¤šā¤˛āĨ‡ ā¤Ēā¤žā¤ āĨā¤¯ā¤•āĨā¤°ā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤¨ā¤žā¤Žā¤žā¤‚ā¤•ā¤¨ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤅ā¤Ēā¤¨ā¤ž ā¤•ā¤žā¤°āĨā¤¯ā¤•āĨā¤°ā¤Ž ā¤ĻāĨ‡ā¤–āĨ‡ā¤‚āĨ¤ + ā¤•ā¤žā¤°āĨā¤¯ā¤•āĨā¤°ā¤Ž ā¤ĩā¤ŋā¤ĩ⤰⤪ + ā¤Ŧā¤§ā¤žā¤ˆ ā¤šāĨ‹! ⤆ā¤Ē⤍āĨ‡ ⤅ā¤Ēā¤¨ā¤ž ā¤Ēā¤žā¤ āĨā¤¯ā¤•āĨā¤°ā¤Ž ā¤ĒāĨ‚ā¤°ā¤ž ⤕⤰ ⤞ā¤ŋā¤¯ā¤ž ā¤šāĨˆāĨ¤ \'⤏āĨ€ā¤–āĨ‡ā¤‚\' ā¤ĒāĨ‡ā¤œ ā¤Ē⤰ ⤅ā¤Ē⤍āĨ€ ā¤ĒāĨā¤°ā¤—⤤ā¤ŋ ⤔⤰ ā¤ĻāĨ‡ā¤–āĨ‡ā¤‚āĨ¤ + ā¤Ēā¤žā¤ āĨā¤¯ā¤•āĨā¤°ā¤Ž ⤚āĨā¤¨āĨ‡ā¤‚ + ā¤Ŧ⤂ā¤Ļ ⤕⤰āĨ‡ā¤‚ + ā¤ŦāĨā¤žā¤¯ā¤ž ā¤—ā¤¯ā¤ž + ⤏⤂⤕āĨā¤ˇā¤ŋā¤ĒāĨā¤¤ ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž + ā¤ŦāĨā¤žā¤ā¤‚ + ⤏⤂⤕āĨā¤ˇā¤ŋā¤ĒāĨā¤¤ ⤕⤰āĨ‡ā¤‚ + ā¤ĒāĨ‚ā¤°ā¤ž ā¤šāĨā¤† + ā¤ĒāĨ‚ā¤°ā¤ž ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨā¤† + ⤞āĨ‰ā¤• ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž + ā¤…ā¤šā¤¯ā¤¨ā¤ŋ⤤ diff --git a/libs/horizon/src/main/res/values-ht/strings.xml b/libs/horizon/src/main/res/values-ht/strings.xml index 97c3ea97b7..f51fc5917b 100644 --- a/libs/horizon/src/main/res/values-ht/strings.xml +++ b/libs/horizon/src/main/res/values-ht/strings.xml @@ -67,6 +67,7 @@ Apèsi NÃ˛t NÃ˛t + Zouti Non devwa (A-Z) Dat limit (pi resan anvan) Non: %1$s @@ -171,9 +172,10 @@ Echèk aktyalizasyon notifikasyon yo Echwe pou chaje notifikasyon yo Rezilta %1$s yo disponib kounye a - NÃ˛t atribisyon - Delè chanje - Modifikasyon ponderasyon notasyon + NÃ˛t chanje + Delè + NÃ˛t + Anons Anons de %1$s Ponderasyon nÃ˛t %1$s yo te modifye %1$s dwe remèt %2$s @@ -346,4 +348,35 @@ Tout Pati nan %s Kou a paka ouvri. + Pati nan %s + Bienveni! Konsilte pwogram ou an pou w ka enskri nan premye kou w. + Pwogram mwen + Enposib pou enskri nan kou. + Echwe pou chaje pwogram yo ak kou yo. + Echwe pou aktyalize pwogram yo ak kou yo. + Jodi a + Yè + Pa kab ouvri Notifikasyon + Pa gen zouti ekstèn pou kou sa a. + BlÃ˛k nÃ˛t + Notifikasyon + Bwat resepsyon + Fich Kou %1$d sou %2$d + %1$d\%% konplete + Nou pa t kapab chaje kontni sa a. + Tanpri eseye ankÃ˛. + Re eseye + Bienveni! Konsilte pwogram ou an pou w ka enskri nan premye kou w. + Detay pwogram + Konpliman! Ou fini kou w la. Gade pwogrè ou ak rezilta ou sou paj Aprantisaj la. + Seleksyone Kou + Fèmen + Elaji + Redwi + Elaji + Redwi + Fini + Poko fini + Bloke + Pa seleksyone diff --git a/libs/horizon/src/main/res/values-id/strings.xml b/libs/horizon/src/main/res/values-id/strings.xml index 997c7060f8..14928a1f50 100644 --- a/libs/horizon/src/main/res/values-id/strings.xml +++ b/libs/horizon/src/main/res/values-id/strings.xml @@ -67,6 +67,7 @@ Tinjauan Umum Skor Catatan + Alat Nama Tugas (A-Z) Tanggal Batas (terbaru lebih dulu) Nama: %1$s @@ -171,9 +172,10 @@ Gagal menyegarkan notifikasi Gagal memuat notifikasi Skor %1$s sudah tersedia - Tugas dinilai - Tanggal batas berubah - Bobot penilaian berubah + Skor berubah + Batas Tanggal + Skor + Pengumuman Pengumuman dari %1$s Bobot skor %1$s berubah %1$s jatuh tempo pada %2$s @@ -346,4 +348,35 @@ Semua Bagian dari %s Kursus tidak dapat dibuka. + Bagian dari %s + Selamat datang! Lihat program Anda untuk mendaftar kursus pertama Anda. + Program saya + Gagal mendaftar di kursus. + Gagal memuat program dan kursus. + Gagal menyegarkan program dan kursus. + Hari Ini + Kemarin + Tidak dapat membuka Notifikasi + Tidak ada alat eksternal untuk kursus ini. + Buku Catatan + Notifikasi + Kotak Masuk + Kartu Kursus %1$d dari %2$d + %1$d\%% selesai + Kami tidak dapat memuat konten ini. + Silakan coba lagi. + Coba lagi + Selamat datang! Lihat program Anda untuk mendaftar kursus pertama Anda. + Detail program + Selamat! Anda telah menyelesaikan kursus Anda. Lihat kemajuan dan skor Anda di halaman Belajar. + Pilih Kursus + Tutup + Diperbesar + Diperkecil + Perbesar + Perkecil + Selesai + Belum selesai + Terkunci + Tidak dipilih diff --git a/libs/horizon/src/main/res/values-is/strings.xml b/libs/horizon/src/main/res/values-is/strings.xml index c41143d9c9..658e4709e5 100644 --- a/libs/horizon/src/main/res/values-is/strings.xml +++ b/libs/horizon/src/main/res/values-is/strings.xml @@ -67,6 +67,7 @@ Yfirlit Einkunnir Athugasemdir + VerkfÃĻri Heiti verkefnis (A-Ö) Skiladagur (nÃŊjast fyrst) Nafn: %1$s @@ -171,9 +172,10 @@ MistÃŗkst að endurnÃŊja tilkynningar Ekki tÃŗkst að hlaða inn tilkynningum Einkunn %1$s er nÃēna tiltÃĻk - Verkefni gefið einkunn - Skiladegi breytt - EinkunnavÃĻgi breytt + Einkunn breytt + Skiladagur + Einkunn + Tilkynning Tilkynning frÃĄ %1$s VÃĻgi einkunnar %1$s var breytt %1$s er með skilafrest %2$s @@ -346,4 +348,35 @@ Allt Hluti af %s Ekki er hÃĻgt að opna nÃĄmskeiðið. + Hluti af %s + Velkomin(n)! Skoðaðu brautina Þína til að innrita Þig í fyrsta nÃĄmskeiðið. + Brautin mín + Ekki tÃŗkst að innrita í nÃĄmskeið. + Ekki tÃŗkst að hlaða inn brautum og nÃĄmskeiðum. + Ekki tÃŗkst að endurnÃŊja brautir og nÃĄmskeið. + Í dag + Í gÃĻr + Ekki tÃŗkst að opna Tilkynningu + Engin ytri verkfÃĻri eru fyrir Þetta nÃĄmskeið. + MinnisbÃŗk + Tilkynningar + InnhÃŗlf + NÃĄmskeiðsspjald %1$d af %2$d + %1$d\%% lokið + Við gÃĄtum ekki hlaðið Þetta efni. + Reyndu aftur. + Reyna aftur + Velkomin(n)! Skoðaðu brautina Þína til að innrita Þig í fyrsta nÃĄmskeiðið. + UpplÃŊsingar um braut + Til hamingju! ÞÃē hefur lokið nÃĄmskeiðinu Þínu. Skoðaðu framvindu Þína og einkunn ÃĄ síðunni LÃĻra. + Velja nÃĄmskeið + Loka + StÃĻkkað + Fellt saman + StÃĻkka + Fella + Lokið + Ekki lokið + LÃĻst + Afvalið diff --git a/libs/horizon/src/main/res/values-it/strings.xml b/libs/horizon/src/main/res/values-it/strings.xml index daf561e185..787f57ba4c 100644 --- a/libs/horizon/src/main/res/values-it/strings.xml +++ b/libs/horizon/src/main/res/values-it/strings.xml @@ -67,6 +67,7 @@ Panoramica Punteggi Note + Strumenti Nome compito (A-Z) Data di scadenza (dal piÚ recente) Nome: %1$s @@ -171,9 +172,10 @@ Impossibile aggiornare notifiche Impossibile caricare notifiche Il punteggio di %1$s adesso è disponibile - Punteggio compito calcolato - Data di scadenza modificata - Peso del punteggio modificato + Punteggio cambiato + Data di scadenza + Punteggio + Annuncio Annuncio da %1$s Il peso del punteggio di %1$s è stato cambiato %1$s scade il %2$s @@ -346,4 +348,35 @@ Tutto Parte di %s Impossibile aprire il corso. + Parte di %s + Benvenuto! Consulta il nostro programma e iscriviti al tuo primo corso. + Il mio programma + Impossibile iscriversi al corso. + Impossibile caricare programmi e corsi. + Impossibile aggiornare programmi e corsi. + Oggi + Ieri + Impossibile aprire la notifica + Non sono presenti strumenti esterni per questo corso. + Quaderno + Notifiche + Posta in arrivo + Scheda corso %1$d di %2$d + %1$d\%% di completamento + Non siamo riusciti a caricare questo contenuto. + Riprova. + Riprova + Benvenuto! Consulta il nostro programma e iscriviti al tuo primo corso. + Dettagli programma + Congratulazioni! Hai completato il corso. Visualizza i tuoi progressi e punteggi nella pagina Apprendi. + Seleziona corso + Chiudi + Esteso + Compresso + Espandi + Comprimi + Completato + Non completato + Bloccato + Non selezionato diff --git a/libs/horizon/src/main/res/values-ja/strings.xml b/libs/horizon/src/main/res/values-ja/strings.xml index 179145a0e0..5c0368f7c0 100644 --- a/libs/horizon/src/main/res/values-ja/strings.xml +++ b/libs/horizon/src/main/res/values-ja/strings.xml @@ -66,6 +66,7 @@ æĻ‚čρ ã‚šã‚ŗã‚ĸ ãƒĄãƒĸ + ツãƒŧãƒĢ čĒ˛éĄŒå (A-Z) 期æ—Ĩīŧˆæœ€æ–°ãŽã‚‚ぎを最初ãĢīŧ‰ 名前īŧš%1$s @@ -167,9 +168,10 @@ 通įŸĨぎ更新ãĢå¤ąæ•—ã—ãžã—ãŸ 通įŸĨぎčĒ­ãŋčžŧãŋãĢå¤ąæ•—ã—ãžã—ãŸã€‚ %1$sãŽã‚šã‚ŗã‚ĸãŒįžåœ¨åˆŠį”¨å¯čƒŊです - čĒ˛éĄŒãŒæŽĄį‚šã•ã‚Œãžã—ãŸ - 期限が変更されぞした - ã‚šã‚ŗã‚ĸぎ重ãŋが変更されぞした + ã‚šã‚ŗã‚ĸ変更済ãŋ + 期限 + ã‚šã‚ŗã‚ĸ + ã‚ĸナã‚Ļãƒŗã‚šãƒĄãƒŗãƒˆ %1$sからぎã‚ĸナã‚Ļãƒŗã‚š %1$sãŽã‚šã‚ŗã‚ĸぎ重ãŋが変更されぞした %1$sは、%2$sが期限です @@ -340,4 +342,35 @@ すずãĻ %sぎ一部 ã‚ŗãƒŧ゚を開けぞせん + %sぎ一部 + ようこそīŧãƒ—ãƒ­ã‚°ãƒŠãƒ ã‚’čĄ¨į¤ēしãĻã€æœ€åˆãŽã‚ŗãƒŧ゚ãĢį™ģéŒ˛ã—ãĻください。 + マイプログナム + ã‚ŗãƒŧ゚ãĢį™ģéŒ˛ã§ããžã›ã‚“ã§ã—ãŸã€‚ + ãƒ—ãƒ­ã‚°ãƒŠãƒ ã¨ã‚ŗãƒŧ゚ぎčĒ­ãŋčžŧãŋãĢå¤ąæ•—ã—ãžã—ãŸã€‚ + ãƒ—ãƒ­ã‚°ãƒŠãƒ ã¨ã‚ŗãƒŧ゚ぎ更新ãĢå¤ąæ•—ã—ãžã—ãŸã€‚ + ä슿—Ĩ + 昨æ—Ĩ + 通įŸĨを開けぞせん。 + ã“ãŽã‚ŗãƒŧ゚ãĢ外部ぎツãƒŧãƒĢはありぞせん。 + ノãƒŧトパã‚Ŋã‚ŗãƒŗ + 通įŸĨ + 受äŋĄãƒˆãƒŦイ + ã‚ŗãƒŧ゚ã‚Ģãƒŧド %1$d / %2$d + %1$d\%% įĩ‚äē† + ã“ãŽã‚ŗãƒŗãƒ†ãƒŗãƒ„ã‚’čĒ­ãŋčžŧめぞせんでした。 + もう一åēĻã‚„ã‚Šį›´ã—ãĻください。 + 再čŠĻ行 + ようこそīŧãƒ—ãƒ­ã‚°ãƒŠãƒ ã‚’čĄ¨į¤ēしãĻã€æœ€åˆãŽã‚ŗãƒŧ゚ãĢį™ģéŒ˛ã—ãĻください。 + ãƒ—ãƒ­ã‚°ãƒŠãƒ ãŽčŠŗį´° + おめでとうございぞすīŧã‚ŗãƒŧ゚を厌äē†ã—ぞした。å­Ļįŋ’ペãƒŧã‚¸ã§é€˛æ—įŠļæŗã¨ã‚šã‚ŗã‚ĸをįĸēčĒã—ãĻください。 + ã‚ŗãƒŧ゚ぎ選択 + 閉じる + æ‹Ąå¤§ + 折りたたãŋ + åą•é–‹ + 折りたたãŋ + 厌äē† + 厌äē†ã—ãĻいぞせん + ロックされãĻいぞす + é¸æŠžč§Ŗé™¤ã•ã‚Œãžã—ãŸ diff --git a/libs/horizon/src/main/res/values-mi/strings.xml b/libs/horizon/src/main/res/values-mi/strings.xml index 9ef2c68242..c274181a1a 100644 --- a/libs/horizon/src/main/res/values-mi/strings.xml +++ b/libs/horizon/src/main/res/values-mi/strings.xml @@ -67,6 +67,7 @@ Tirohanga whānui Ngā Kaute He Karere Poto + Ngā Taputapu Ingoa Taumahi (A-Z) Te ra mutunga (hou tuatahi) Ingoa: %1$s @@ -171,9 +172,10 @@ I rahua te whakahou i nga whakamohiotanga I rahua te uta whakamohiotanga %1$s Kei te watea te tatau a - Kua tohua te taumahi - I huri te ra tika - Kua huri te taumaha o te kaute + Kua huri te kaute + Rā tika + Tohu + Pānui Panuitanga mai i %1$s %1$s I hurihia te taumaha o te kaute %1$s kua tae ki te %2$s @@ -346,4 +348,35 @@ Katoa Wāhanga o %s Kaore e taea te whakatuwhera i te akoranga. + Wāhanga o %s + Nau Mai! Tirohia to hotaka ki te whakauru ki to akoranga tuatahi. + Taku kaupapa + I rahua te whakauru ki te akoranga. + I rahua te uta i nga papatono me nga akoranga. + I rahua te whakahou i nga papatono me nga akoranga. + I tēnei rā tonu + Inanahi + Kaore e taea te whakatuwhera i te Panui + Karekau he taputapu o waho mo tenei akoranga. + Pukatuhituhi + Whakamōhiotanga + Pouakauru + Kaari Akoranga %1$d o %2$d + %1$d\%% oti + Kaore i taea e matou te uta i tenei ihirangi. + Tēnā, whakamātau anō. + Ngana ano + Nau Mai! Tirohia to hotaka ki te whakauru ki to akoranga tuatahi. + Ngā taipitopito hōtaka + Ngā mihi! Kua oti i a koe to akoranga. Tirohia ki te ahunga whakamua me nga kaute i te wharangi Ako. + TÄĢpako Akoranga + Katia + Kua rahi ake + Hinga + Whakawhānui + Tiango + Kua Oti + Kāore i oti + Kua rakaina + Kāore i tÄĢpakohia diff --git a/libs/horizon/src/main/res/values-ms/strings.xml b/libs/horizon/src/main/res/values-ms/strings.xml index f5e2833476..18c6619bd3 100644 --- a/libs/horizon/src/main/res/values-ms/strings.xml +++ b/libs/horizon/src/main/res/values-ms/strings.xml @@ -67,6 +67,7 @@ Gambaran Keseluruhan Skor Nota + Alat Nama tugasan (A-Z) Tarikh siap (terbaru dahulu) Nama: %1$s @@ -171,9 +172,10 @@ Gagal menyegar semula pemberitahuan Gagal memuatkan pemberitahuan Skor %1$s kini tersedia - Tugasan diberikan skor - Tarikh siap berubah - Pemberat skor berubah + Skor berubah + Tarikh Siap + Skor + Pengumuman Pengumuman daripada %1$s Pemberat skor %1$s telah diubah %1$s perlu siap papa %2$s @@ -346,4 +348,35 @@ Semua Bahagian daripada %s Kursus tidak dapat dibuka. + Bahagian daripada %s + Selamat datang! Lihat program anda untuk mendaftar dalam kursus pertama anda. + Program saya + Gagal untuk mendaftar dalam kursus. + Gagal untuk memuatkan program dan kursus. + Gagal untuk menyegar semula program dan kursus. + Hari Ini + Semalam + Tidak dapat membuka Pemberitahuan + Tiada alat luaran untuk kursus ini. + Buku Nota + Pemberitahuan + Peti masuk + Kad Kursus %1$d daripada %2$d + %1$d\%% selesai + Kami tidak dapat memuatkan kandungan ini. + Sila cuba semula. + Cuba semula + Selamat datang! Lihat program anda untuk mendaftar dalam kursus pertama anda. + Butiran program + Tahniah! Anda telah menyelesaikan kursus anda. Lihat kemajuan dan markah anda pada halaman Belajar. + Pilih Kursus + Tutup + Kembangkan + Dikuncupkan + Kembangkan + Kuncupkan + Dilengkapkan + Tidak lengkap + Berkunci + Nyahpilih diff --git a/libs/horizon/src/main/res/values-nb/strings.xml b/libs/horizon/src/main/res/values-nb/strings.xml index c7d5be5965..6aa41d50a4 100644 --- a/libs/horizon/src/main/res/values-nb/strings.xml +++ b/libs/horizon/src/main/res/values-nb/strings.xml @@ -67,6 +67,7 @@ Oversikt Vurderinger Merknader + Verktøy Oppgavenavn – (A–Z) Forfallsdato (nyeste først) Navn: %1$s @@ -171,9 +172,10 @@ Kunne ikke oppdatere varslinger Kunne ikke laste inn varslinger %1$s sin poengsum er nÃĨ tilgjengelig - Oppgaver vurdert - Forfallsdato endret - Vurderingsvekting endret + Poengsum endret + Forfallsdato + Resultat + Kunngjøring Kunngjøring fra %1$s %1$s sin vurderingsvekting ble endret %1$s forfaller %2$s @@ -346,4 +348,35 @@ Alle Del av %s Dette emnet kan ikke ÃĨpnes. + Del av %s + Velkommen! Vis programmet ditt for ÃĨ melde deg pÃĨ ditt første emne. + Programmet mitt + Kunne ikke melde deg pÃĨ i emnet. + Kunneikke laste inn programmer og emner. + Kunne ikke oppdatere programmer og emner. + I dag + I gÃĨr + Kunne ikke ÃĨpne varsling + Det er ingen eksterne verktøy for dette emnet. + Notatbok + Varslinger + Innboks + Emnekort %1$d av %2$d + %1$d\%% fullført + Vi kunne ikke laste inn dette innholdet. + Prøv pÃĨ nytt. + Prøv igjen + Velkommen! Vis programmet ditt for ÃĨ melde deg pÃĨ ditt første emne. + Programdetaljer + Gratulerer! Du har fullført emnet ditt. Se fremgangen og poengsummene dine pÃĨ LÃĻr-siden. + Velg emne + Lukk + Utvidet + Skjult + Utvid + Skjul + Godkjent + Ikke godkjent + LÃĨst + Ikke valgt diff --git a/libs/horizon/src/main/res/values-nl/strings.xml b/libs/horizon/src/main/res/values-nl/strings.xml index 74bb2f8d5d..b056220fec 100644 --- a/libs/horizon/src/main/res/values-nl/strings.xml +++ b/libs/horizon/src/main/res/values-nl/strings.xml @@ -67,6 +67,7 @@ Overzicht Scores Opmerkingen + Tools Opdrachtnaam (A-Z) Vervaldatum (nieuwste eerst) Naam: %1$s @@ -171,9 +172,10 @@ Kan meldingen niet vernieuwen Kan meldingen niet laden De score van %1$s is nu beschikbaar - Opdracht beoordeeld - Inleverdatum gewijzigd - Gewicht score gewijzigd + Score gewijzigd + Inleverdatum + Score + Aankondiging Aankondiging van %1$s Het gewicht van de score van %1$s is gewijzigd %1$s dient op %2$s te worden ingeleverd @@ -346,4 +348,35 @@ Alles Onderdeel van %s De cursus kan niet worden geopend. + Onderdeel van %s + Welkom! Bekijk ons programma om je in te schrijven voor je eerste cursus. + Mijn programma + Inschrijven voor de cursus mislukt. + Laden van programma\'s en cursussen mislukt. + Vernieuwen van programma\'s en cursussen mislukt. + Vandaag + Gisteren + Kan Melding niet openen + Er zijn geen externe tools voor deze cursus. + Notitieblok + Meldingen + Inbox + Cursuskaart %1$d van %2$d + %1$d\%% voltooid + We konden deze inhoud niet laden. + Probeer het opnieuw. + Opnieuw proberen + Welkom! Bekijk ons programma om je in te schrijven voor je eerste cursus. + Programmadetails + Gefeliciteerd! Je hebt je cursus voltooid. Bekijk je voortgang en scores op de pagina Leren. + Cursus selecteren + Sluiten + Uitgevouwen + Samengevouwen + Uitvouwen + Samenvouwen + Voltooid + Niet voltooid + Vergrendeld + Niet geselecteerd diff --git a/libs/horizon/src/main/res/values-pl/strings.xml b/libs/horizon/src/main/res/values-pl/strings.xml index b1c37bd3e7..7a37bb718a 100644 --- a/libs/horizon/src/main/res/values-pl/strings.xml +++ b/libs/horizon/src/main/res/values-pl/strings.xml @@ -69,6 +69,7 @@ OmÃŗwienie Wyniki Uwagi + Narzędzia Nazwa zadania (A-Z) Termin (od najnowszych) Nazwa: %1$s @@ -179,9 +180,10 @@ Nie udało się odświeÅŧyć powiadomień Nie udało się wczytać powiadomień Dostępny jest teraz wynik %1$s - Ocenione zadanie - Termin został zmieniony - Waga wyniku została zmieniona + Wynik uległ zmianie + Termin + Wynik + Ogłoszenie Ogłoszenie od %1$s Zmieniono wagę wyniku %1$s Dla %1$s termin to %2$s @@ -358,4 +360,35 @@ Wszystkie Część z %s Nie moÅŧna otworzyć kursu. + Część z %s + Witamy! Wyświetl program, aby zarejestrować się w pierwszym kursie. + MÃŗj program + Nie udało się zarejestrować w kursie. + Nie udało się wczytać programÃŗw i kursÃŗw. + Nie udało się odświeÅŧyć programÃŗw i kursÃŗw. + Dzisiaj + Wczoraj + Nie udało się otworzyć powiadomienia + Brak zewnętrznych narzędzi wymaganych dla tego kursu. + Notatnik + Powiadomienia + Skrzynka odbiorcza + Karta kursu %1$d z %2$d + %1$d\%% ukończenia. + Nie udało się wczytać tej zawartości. + SprÃŗbuj ponownie. + PonÃŗw prÃŗbę + Witamy! Wyświetl program, aby zarejestrować się w pierwszym kursie. + SzczegÃŗÅ‚y programu + Gratulacje! Ukończono juÅŧ ten kurs. Wyświetl postępy i wyniki na stronie Nauka. + Wybierz kurs + Zamknij + Rozwinięte + Schowany + Rozwiń + Zwiń + Ukończono + Nie ukończono + Zablokowany + Niewybrane diff --git a/libs/horizon/src/main/res/values-pt-rBR/strings.xml b/libs/horizon/src/main/res/values-pt-rBR/strings.xml index 0199cb76e3..6140e28762 100644 --- a/libs/horizon/src/main/res/values-pt-rBR/strings.xml +++ b/libs/horizon/src/main/res/values-pt-rBR/strings.xml @@ -67,6 +67,7 @@ VisÃŖo geral PontuaçÃĩes Notas + Ferramentas Nome da tarefa (A-Z) Data de vencimento (a mais recente primeiro) Nome: %1$s @@ -171,9 +172,10 @@ Falha ao atualizar as notificaçÃĩes Falha ao carregar as notificaçÃĩes A pontuaÃ§ÃŖo de %1$s jÃĄ estÃĄ disponível - Tarefa pontuada - Data de entrega alterada - Peso da pontuaÃ§ÃŖo alterado + A pontuaÃ§ÃŖo foi alterada + Data de vencimento + PontuaÃ§ÃŖo + Aviso AnÃēncio de %1$s O peso da pontuaÃ§ÃŖo de %1$s foi alterado %1$s deve ser entregue em %2$s @@ -346,4 +348,35 @@ Todos Parte de %s O curso nÃŖo pode ser aberto. + Parte de %s + Bem-vindo(a)! Veja seu programa para se matricular em seu primeiro curso. + Meu programa + Falha ao se matricular no curso. + Falha ao carregar programas e cursos. + Falha na atualizaÃ§ÃŖo de programas e cursos. + Hoje + Ontem + NÃŖo Ê possível abrir a NotificaÃ§ÃŖo + NÃŖo hÃĄ ferramentas externas para este curso. + Caderno + NotificaçÃĩes + Caixa de entrada + CartÃŖo de curso %1$d de %2$d + %1$d\%% concluído + NÃŖo foi possível carregar esse conteÃēdo. + Por favor, tente novamente. + Tentar novamente + Bem-vindo(a)! Veja seu programa para se matricular em seu primeiro curso. + Detalhes do programa + ParabÊns! VocÃĒ concluiu seu curso. Veja seu progresso e pontuaçÃĩes na pÃĄgina Aprender. + Selecionar curso + Fechar + Expandido + Recolhido + Expandir + Recolher + Completo + NÃŖo concluído + Bloqueado(a) + NÃŖo selecionado diff --git a/libs/horizon/src/main/res/values-pt-rPT/strings.xml b/libs/horizon/src/main/res/values-pt-rPT/strings.xml index de909e059b..b40ede6506 100644 --- a/libs/horizon/src/main/res/values-pt-rPT/strings.xml +++ b/libs/horizon/src/main/res/values-pt-rPT/strings.xml @@ -67,6 +67,7 @@ VisÃŖo geral PontuaÃ§ÃŖo Notas + Ferramentas Nome da tarefa (A-Z) Data de entrega (mais recentes primeiro) Nome: %1$s @@ -171,9 +172,10 @@ Falha ao atualizar as notificaçÃĩes. Falha ao carregar notificaçÃĩes A pontuaÃ§ÃŖo %1$s\ jÃĄ estÃĄ disponível. - Tarefa pontuada - Data de vencimento alterada - PonderaÃ§ÃŖo da pontuaÃ§ÃŖo alterada + PontuaÃ§ÃŖo alterada + Data de limite + PontuaÃ§ÃŖo + AnÃēncio AnÃēncio de %1$s A ponderaÃ§ÃŖo da pontuaÃ§ÃŖo %1$s\ foi alterada. %1$s Ê devido em %2$s @@ -346,4 +348,35 @@ Todos Parte de %s NÃŖo Ê possível abrir a disciplina. + Parte de %s + Bem-vindo! Veja o seu programa para se inscrever na sua primeira disciplina. + O meu programa + Falha ao inscrever-se na disciplina. + Falha ao carregar programas e disciplinas. + Falha ao atualizar programas e disciplinas. + Hoje + Ontem + NÃŖo Ê possível abrir a notificaÃ§ÃŖo + NÃŖo hÃĄ ferramentas externas para esta disciplina. + Caderno + NotificaçÃĩes + Caixa de entrada + CartÃŖo da disciplina %1$d de %2$d + %1$d\%% completo + NÃŖo foi possível carregar este conteÃēdo. + Tente novamente. + Tentar novamente + Bem-vindo! Veja o seu programa para se inscrever na sua primeira disciplina. + Detalhes do programa + ParabÊns! Concluiu a sua disciplina. Veja o seu progresso e pontuaÃ§ÃŖo na pÃĄgina Aprender. + Selecionar disciplina + Fechar + Expandido + Fechado + Expandir + Recolher + Concluído + NÃŖo estÃĄ completo + Bloqueado + NÃŖo selecionado diff --git a/libs/horizon/src/main/res/values-ru/strings.xml b/libs/horizon/src/main/res/values-ru/strings.xml index d765577a89..6b9de8b00c 100644 --- a/libs/horizon/src/main/res/values-ru/strings.xml +++ b/libs/horizon/src/main/res/values-ru/strings.xml @@ -69,6 +69,7 @@ ĐžĐąĐˇĐžŅ€ БаĐģĐģŅ‹ ЗаĐŧĐĩŅ‚Đēи + ИĐŊŅŅ‚Ņ€ŅƒĐŧĐĩĐŊ҂ҋ ИĐŧŅ СадаĐŊĐ¸Ņ (A-Z) ĐĄŅ€ĐžĐē Đ˛Ņ‹ĐŋĐžĐģĐŊĐĩĐŊĐ¸Ņ (ҁĐŊĐ°Ņ‡Đ°Đģа ŅĐ°ĐŧŅ‹Đš ĐŋĐžŅĐģĐĩĐ´ĐŊиК) ИĐŧŅ: %1$s @@ -179,9 +180,10 @@ НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ОйĐŊĐžĐ˛Đ¸Ņ‚ŅŒ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊĐ¸Ņ НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊĐ¸Ņ ĐžŅ†ĐĩĐŊĐēа %1$s Ņ‚ĐĩĐŋĐĩŅ€ŅŒ Đ´ĐžŅŅ‚ŅƒĐŋĐŊа - ĐžŅ†ĐĩĐŊĐĩĐŊĐŊĐ°Ņ ĐˇĐ°Đ´Đ°Ņ‡Đ° - Đ”Đ°Ņ‚Đ° Đ˛Ņ‹ĐŋĐžĐģĐŊĐĩĐŊĐ¸Ņ иСĐŧĐĩĐŊĐĩĐŊа - ВĐĩҁ ĐžŅ†ĐĩĐŊиваĐŊĐ¸Ņ иСĐŧĐĩĐŊĐĩĐŊ + ĐžŅ†ĐĩĐŊĐēа иСĐŧĐĩĐŊĐĩĐŊа + Đ”Đ°Ņ‚Đ° Đ˛Ņ‹ĐŋĐžĐģĐŊĐĩĐŊĐ¸Ņ + ĐžŅ†ĐĩĐŊĐēа + ĐžĐąŅŠŅĐ˛ĐģĐĩĐŊиĐĩ ĐžĐąŅŠŅĐ˛ĐģĐĩĐŊиĐĩ ĐžŅ‚ %1$s ВĐĩҁ ĐžŅ†ĐĩĐŊĐēи %1$s ĐąŅ‹Đģ иСĐŧĐĩĐŊĐĩĐŊ ĐĄŅ€ĐžĐē ŅĐ´Đ°Ņ‡Đ¸ %1$s %2$s @@ -358,4 +360,35 @@ Đ’ŅĐĩ Đ§Đ°ŅŅ‚ŅŒ иС %s ĐšŅƒŅ€Ņ ĐŊĐĩĐģŅŒĐˇŅ ĐžŅ‚ĐēŅ€Ņ‹Ņ‚ŅŒ. + Đ§Đ°ŅŅ‚ŅŒ иС %s + Đ”ĐžĐąŅ€Đž ĐŋĐžĐļаĐģĐžĐ˛Đ°Ņ‚ŅŒ! ОзĐŊаĐēĐžĐŧŅŒŅ‚ĐĩҁҌ ŅĐž ŅĐ˛ĐžĐĩĐš ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŧОК Đ´ĐģŅ ĐˇĐ°Ņ‡Đ¸ŅĐģĐĩĐŊĐ¸Ņ ĐŊа ŅĐ˛ĐžĐš ĐŋĐĩŅ€Đ˛Ņ‹Đš ĐēŅƒŅ€Ņ. + ĐœĐžŅ ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŧа + ĐžŅˆĐ¸ĐąĐēа ĐˇĐ°Ņ‡Đ¸ŅĐģĐĩĐŊĐ¸Ņ ĐŊа ĐēŅƒŅ€Ņ. + НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŧŅ‹ и ĐēŅƒŅ€ŅŅ‹. + НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ОйĐŊĐžĐ˛Đ¸Ņ‚ŅŒ ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŧŅ‹ и ĐēŅƒŅ€ŅŅ‹. + ĐĄĐĩĐŗĐžĐ´ĐŊŅ + Đ’Ņ‡ĐĩŅ€Đ° + НĐĩ ŅƒĐ´Đ°ĐĩŅ‚ŅŅ ĐžŅ‚ĐēŅ€Ņ‹Ņ‚ŅŒ ĐŖĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊиĐĩ + НĐĩŅ‚ вĐŊĐĩ҈ĐŊĐ¸Ņ… иĐŊŅŅ‚Ņ€ŅƒĐŧĐĩĐŊŅ‚ĐžĐ˛ Đ´ĐģŅ ŅŅ‚ĐžĐŗĐž ĐēŅƒŅ€ŅĐ°. + БĐģĐžĐēĐŊĐžŅ‚ + ĐŖĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊĐ¸Ņ + Đ’Ņ…ĐžĐ´ŅŅ‰Đ¸Đĩ + ĐšĐ°Ņ€Ņ‚ĐžŅ‡Đēи ĐēŅƒŅ€ŅĐ° %1$d иС %2$d + %1$d\%% СавĐĩŅ€ŅˆĐĩĐŊĐž + НаĐŧ ĐŊĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ ŅŅ‚ĐžŅ‚ ĐēĐžĐŊŅ‚ĐĩĐŊŅ‚. + ĐŸĐžĐ˛Ņ‚ĐžŅ€Đ¸Ņ‚Đĩ ĐŋĐžĐŋҋ҂Đē҃. + ĐŸĐžĐ˛Ņ‚ĐžŅ€Đ¸Ņ‚ŅŒ + Đ”ĐžĐąŅ€Đž ĐŋĐžĐļаĐģĐžĐ˛Đ°Ņ‚ŅŒ! ОзĐŊаĐēĐžĐŧŅŒŅ‚ĐĩҁҌ ŅĐž ŅĐ˛ĐžĐĩĐš ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŧОК Đ´ĐģŅ ĐˇĐ°Ņ‡Đ¸ŅĐģĐĩĐŊĐ¸Ņ ĐŊа ŅĐ˛ĐžĐš ĐŋĐĩŅ€Đ˛Ņ‹Đš ĐēŅƒŅ€Ņ. + ДĐĩŅ‚Đ°Đģи ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŧŅ‹ + ĐŸĐžĐˇĐ´Ņ€Đ°Đ˛ĐģŅĐĩĐŧ! Đ’Ņ‹ СавĐĩŅ€ŅˆĐ¸Đģи ŅĐ˛ĐžĐš ĐēŅƒŅ€Ņ. ĐŸŅ€ĐžŅĐŧĐžŅ‚Ņ€Đ¸Ņ‚Đĩ ŅĐ˛ĐžĐš ĐŋŅ€ĐžĐŗŅ€Đĩҁҁ и Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚Ņ‹ ĐŊа ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Đĩ ÂĢĐžĐąŅƒŅ‡ĐĩĐŊиĐĩÂģ. + Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ ĐēŅƒŅ€Ņ + ЗаĐēŅ€Ņ‹Ņ‚ŅŒ + В Ņ€Đ°ĐˇĐ˛ĐĩŅ€ĐŊŅƒŅ‚ĐžĐŧ видĐĩ + В ŅĐ˛ĐĩŅ€ĐŊŅƒŅ‚ĐžĐŧ видĐĩ + РаСвĐĩŅ€ĐŊŅƒŅ‚ŅŒ + ХвĐĩŅ€ĐŊŅƒŅ‚ŅŒ + ЗавĐĩŅ€ŅˆĐĩĐŊĐž + НĐĩ СавĐĩŅ€ŅˆĐĩĐŊĐž + ЗабĐģĐžĐēĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐž + Đ’Ņ‹ĐąĐžŅ€ ĐžŅ‚ĐŧĐĩĐŊĐĩĐŊ diff --git a/libs/horizon/src/main/res/values-sl/strings.xml b/libs/horizon/src/main/res/values-sl/strings.xml index 8f3a9bbb6b..3928d38234 100644 --- a/libs/horizon/src/main/res/values-sl/strings.xml +++ b/libs/horizon/src/main/res/values-sl/strings.xml @@ -67,6 +67,7 @@ Pregled Točke Opombe + Orodja Ime naloge (A–Z) Rok (najprej najnovejÅĄi) Ime: %1$s @@ -171,9 +172,10 @@ OsveÅževanje sporočil ni uspelo Nalaganje sporočil ni uspelo Rezultat za %1$s je sedaj na voljo - Naloga ocenjena - Rok je spremenjen - UteÅž ocenjevanja spremenjena + Rezultat spremenjen + Rok + Rezultat + Obvestilo Obvestilo od %1$s UteÅž ocenjevanja za %1$s je bila spremenjena %1$s ima rok dne %2$s @@ -346,4 +348,35 @@ Vse Del od %s Predmeta ni mogoče odpreti. + Del od %s + DobrodoÅĄli. Za vpis v prvi predmet si oglejte svoj program. + Moj program + Vpis v predmet ni uspel. + Nalaganje programov in predmetov ni uspelo. + OsveÅžitev programov in predmetov ni uspela. + Danes + Včeraj + Sporočila ni mogoče odpreti + Pri tem predmetu ni zunanjih orodij. + BeleÅžnica + Obvestila + PoÅĄta + Kartica predmeta %1$d od %2$d + %1$d\%% dokončano + Te vsebine ni bilo mogoče shraniti. + Poskusite znova. + Ponovno poskusi + DobrodoÅĄli. Za vpis v prvi predmet si oglejte svoj program. + Podrobnosti programa + Čestitke! Predmet ste zaključili. Svoj napredek in rezultate si lahko ogledate na strani Učenje. + Izberi predmet + Zapri + RazÅĄirjeno + Strnjeno + RazÅĄiri + Strni + Zaključeno + Ni zaključeno + Zaklenjeno + Neizbrano diff --git a/libs/horizon/src/main/res/values-sv/strings.xml b/libs/horizon/src/main/res/values-sv/strings.xml index 34ad4929b6..08ae464527 100644 --- a/libs/horizon/src/main/res/values-sv/strings.xml +++ b/libs/horizon/src/main/res/values-sv/strings.xml @@ -67,6 +67,7 @@ Översikt Resultat: Anteckningar + Verktyg Uppgiftsnamn (A–Z) Inlämningsdatum (senaste fÃļrst) Namn: %1$s @@ -171,9 +172,10 @@ Det gick inte att uppdatera aviseringar Det gick inte att läsa in aviseringar Resultatet fÃļr %1$s finns nu tillgängligt - Uppgiften bedÃļmd - Inlämningsdatumet ändrades - Resultatvikten har ändrats + Resultatet har ändrats + Inlämningsdatum + Resultat + Anslag Meddelande frÃĨn %1$s Resultatvikten fÃļr %1$s har ändrats %1$s ska lämnas in %2$s @@ -346,4 +348,35 @@ Alla Del av %s Det gÃĨr inte att Ãļppna kursen. + Del av %s + Välkommen! Visa ditt program fÃļr att registrera dig i din fÃļrsta kurs. + Mitt program + Det gick inte att registrera dig i en kurs. + Det gick inte att läsa in program och kurser. + Det gick inte att uppdatera program och kurser. + I dag + I gÃĨr + Det gick inte att Ãļppna aviseringen + Det finns inga externa verktyg fÃļr den här kursen. + Anteckningsbok + Aviseringar + Inkorg + Kurskort %1$d av %2$d + %1$d\%% slutfÃļrd + Det gick inte att läsa in detta innehÃĨll. + FÃļrsÃļk igen. + FÃļrsÃļk igen + Välkommen! Visa ditt program fÃļr att registrera dig i din fÃļrsta kurs. + Programinformation + Grattis! Du har slutfÃļrt din kurs. Visa dina framsteg och poäng pÃĨ sidan Lär. + Välj kurs + Stäng + Expanderad + Stängd + Visa + DÃļlj + Färdig + Inte färdig + LÃĨst + Omarkerade diff --git a/libs/horizon/src/main/res/values-th/strings.xml b/libs/horizon/src/main/res/values-th/strings.xml index 88d67461a2..9739a67b56 100644 --- a/libs/horizon/src/main/res/values-th/strings.xml +++ b/libs/horizon/src/main/res/values-th/strings.xml @@ -67,6 +67,7 @@ ā¸ ā¸˛ā¸žā¸Ŗā¸§ā¸Ą ā¸„ā¸°āšā¸™ā¸™ ā¸Ģā¸Ąā¸˛ā¸ĸāš€ā¸Ģ⏕⏏ + āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ā¸Ąā¸ˇā¸­ ā¸Šā¸ˇāšˆā¸­ā¸ ā¸˛ā¸Ŗā¸ā¸´ā¸ˆ (A - Z) ā¸§ā¸ąā¸™ā¸„ā¸Ŗā¸šā¸ā¸ŗā¸Ģ⏙⏔ (āšƒā¸Ģā¸Ąāšˆā¸Ēā¸¸ā¸”āš„ā¸›āš€ā¸āšˆā¸˛ā¸Ē⏏⏔) ā¸Šā¸ˇāšˆā¸­: %1$s @@ -171,9 +172,10 @@ āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ŗā¸ĩāš€ā¸Ÿā¸Ŗā¸Šā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāš„ā¸”āš‰ āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš‚ā¸Ģā¸Ĩā¸”ā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāš„ā¸”āš‰ ā¸„ā¸°āšā¸™ā¸™ā¸‚ā¸­ā¸‡ %1$s ā¸Ąā¸ĩāš€ā¸œā¸ĸāšā¸žā¸Ŗāšˆāšā¸Ĩāš‰ā¸§ā¸•ā¸­ā¸™ā¸™ā¸ĩāš‰ - āšƒā¸Ģāš‰ā¸„ā¸°āšā¸™ā¸™ā¸ ā¸˛ā¸Ŗā¸ā¸´ā¸ˆāšā¸Ĩāš‰ā¸§ - āšā¸āš‰āš„ā¸‚ā¸§ā¸ąā¸™ā¸„ā¸Ŗā¸šā¸ā¸ŗā¸Ģā¸™ā¸”āšā¸Ĩāš‰ā¸§ - āšā¸āš‰āš„ā¸‚ā¸™āš‰ā¸ŗā¸Ģā¸™ā¸ąā¸ā¸„ā¸°āšā¸™ā¸™āšā¸Ĩāš‰ā¸§ + ā¸„ā¸°āšā¸™ā¸™ā¸Ąā¸ĩā¸ā¸˛ā¸Ŗāš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āšā¸›ā¸Ĩ⏇ + ā¸§ā¸ąā¸™ā¸„ā¸Ŗā¸šā¸ā¸ŗā¸Ģ⏙⏔ + ā¸„ā¸°āšā¸™ā¸™ + ⏛⏪⏰⏁⏞⏍ ā¸›ā¸Ŗā¸°ā¸ā¸˛ā¸¨ā¸ˆā¸˛ā¸ %1$s ā¸„ā¸°āšā¸™ā¸™ā¸‚ā¸­ā¸‡ %1$s ā¸–ā¸šā¸āšā¸āš‰āš„ā¸‚ %1$s ā¸„ā¸Ŗā¸šā¸ā¸ŗā¸Ģā¸™ā¸”āš€ā¸Ąā¸ˇāšˆā¸­ %2$s @@ -346,4 +348,35 @@ ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸” ā¸Ēāšˆā¸§ā¸™āš€ā¸™ā¸ˇāš‰ā¸­ā¸Ģ⏞⏈⏞⏁ %s āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸´ā¸”ā¸šā¸—āš€ā¸Ŗā¸ĩā¸ĸā¸™āš„ā¸”āš‰ + ā¸Ēāšˆā¸§ā¸™āš€ā¸™ā¸ˇāš‰ā¸­ā¸Ģ⏞⏈⏞⏁ %s + ā¸ĸ⏴⏙⏔ā¸ĩā¸•āš‰ā¸­ā¸™ā¸Ŗā¸ąā¸š! ā¸”ā¸šā¸Ģā¸Ĩā¸ąā¸ā¸Ēā¸šā¸•ā¸Ŗā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš€ā¸žā¸ˇāšˆā¸­ā¸Ĩā¸‡ā¸—ā¸°āš€ā¸šā¸ĩā¸ĸā¸™ā¸šā¸—āš€ā¸Ŗā¸ĩā¸ĸā¸™āšā¸Ŗā¸ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ + ā¸Ģā¸Ĩā¸ąā¸ā¸Ēā¸šā¸•ā¸Ŗā¸‚ā¸­ā¸‡ā¸‰ā¸ąā¸™ + āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩā¸‡ā¸—ā¸°āš€ā¸šā¸ĩā¸ĸā¸™ā¸šā¸—āš€ā¸Ŗā¸ĩā¸ĸā¸™āš„ā¸”āš‰ + āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš‚ā¸Ģā¸Ĩ⏔ā¸Ģā¸Ĩā¸ąā¸ā¸Ēā¸šā¸•ā¸Ŗāšā¸Ĩā¸°ā¸šā¸—āš€ā¸Ŗā¸ĩā¸ĸā¸™āš„ā¸”āš‰ + āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ŗā¸ĩāš€ā¸Ÿā¸Ŗā¸Šā¸Ģā¸Ĩā¸ąā¸ā¸Ēā¸šā¸•ā¸Ŗāšā¸Ĩā¸°ā¸šā¸—āš€ā¸Ŗā¸ĩā¸ĸā¸™āš„ā¸”āš‰ + ā¸§ā¸ąā¸™ā¸™ā¸ĩāš‰ + āš€ā¸Ąā¸ˇāšˆā¸­ā¸§ā¸˛ā¸™ā¸™ā¸ĩāš‰ + āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸´ā¸” ā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ (Notification) āš„ā¸”āš‰ + āš„ā¸Ąāšˆā¸Ąā¸ĩāš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ā¸Ąā¸ˇā¸­ā¸ˆā¸˛ā¸ā¸ ā¸˛ā¸ĸ⏙⏭⏁ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸šā¸—āš€ā¸Ŗā¸ĩā¸ĸ⏙⏙ā¸ĩāš‰ + ā¸Ēā¸Ąā¸¸ā¸” + ā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ + ⏁ā¸Ĩāšˆā¸­ā¸‡ā¸ˆā¸”ā¸Ģā¸Ąā¸˛ā¸ĸ + ā¸ā¸˛ā¸ŖāšŒā¸”ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸šā¸—āš€ā¸Ŗā¸ĩā¸ĸ⏙ %1$d ⏈⏞⏁ %2$d + āš€ā¸Ēā¸Ŗāš‡ā¸ˆā¸Ēā¸´āš‰ā¸™ %1$d\%% + āš€ā¸Ŗā¸˛āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš‚ā¸Ģā¸Ĩā¸”āš€ā¸™ā¸ˇāš‰ā¸­ā¸Ģ⏞⏙ā¸ĩāš‰ + ⏁⏪⏏⏓⏞ā¸Ĩā¸­ā¸‡āšƒā¸Ģā¸Ąāšˆā¸­ā¸ĩā¸ā¸„ā¸Ŗā¸ąāš‰ā¸‡āšƒā¸™ā¸ ā¸˛ā¸ĸā¸Ģā¸Ĩā¸ąā¸‡ + ā¸Ĩā¸­ā¸‡āšƒā¸Ģā¸Ąāšˆ + ā¸ĸ⏴⏙⏔ā¸ĩā¸•āš‰ā¸­ā¸™ā¸Ŗā¸ąā¸š! ā¸”ā¸šā¸Ģā¸Ĩā¸ąā¸ā¸Ēā¸šā¸•ā¸Ŗā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš€ā¸žā¸ˇāšˆā¸­ā¸Ĩā¸‡ā¸—ā¸°āš€ā¸šā¸ĩā¸ĸā¸™ā¸šā¸—āš€ā¸Ŗā¸ĩā¸ĸā¸™āšā¸Ŗā¸ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ + ⏪⏞ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸ⏔ā¸Ģā¸Ĩā¸ąā¸ā¸Ēā¸šā¸•ā¸Ŗ + ā¸ĸ⏴⏙⏔ā¸ĩā¸”āš‰ā¸§ā¸ĸ! ā¸„ā¸¸ā¸“ā¸ˆā¸šā¸šā¸—āš€ā¸Ŗā¸ĩā¸ĸā¸™āšā¸Ĩāš‰ā¸§ ā¸”ā¸šā¸Ēā¸–ā¸˛ā¸™ā¸°āšā¸Ĩā¸°ā¸„ā¸°āšā¸™ā¸™ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš„ā¸”āš‰ā¸ˆā¸˛ā¸ā¸Ģā¸™āš‰ā¸˛ āš€ā¸Ŗā¸ĩā¸ĸā¸™ā¸Ŗā¸šāš‰ + āš€ā¸Ĩā¸ˇā¸­ā¸ā¸šā¸—āš€ā¸Ŗā¸ĩā¸ĸ⏙ + ⏛⏴⏔ + ⏂ā¸ĸ⏞ā¸ĸāšā¸Ĩāš‰ā¸§ + ā¸ĸāšˆā¸­āšā¸Ĩāš‰ā¸§ + ⏂ā¸ĸ⏞ā¸ĸ + ā¸ĸāšˆā¸­ + āš€ā¸Ēā¸Ŗāš‡ā¸ˆā¸Ēā¸´āš‰ā¸™āšā¸Ĩāš‰ā¸§ + āš„ā¸Ąāšˆāš€ā¸Ēā¸Ŗāš‡ā¸ˆā¸Ēā¸´āš‰ā¸™ + ā¸Ĩāš‡ā¸­ā¸„āšā¸Ĩāš‰ā¸§ + āš„ā¸Ąāšˆāš„ā¸”āš‰āš€ā¸Ĩ⏎⏭⏁ diff --git a/libs/horizon/src/main/res/values-vi/strings.xml b/libs/horizon/src/main/res/values-vi/strings.xml index 8c86d9fd11..ac4bcba4c0 100644 --- a/libs/horizon/src/main/res/values-vi/strings.xml +++ b/libs/horizon/src/main/res/values-vi/strings.xml @@ -67,6 +67,7 @@ Táģ•ng Quan Điáģƒm Lưu Ý + Công CáģĨ TÃĒn bài táē­p (A-Z) Ngày đáēŋn háēĄn (máģ›i nháēĨt trưáģ›c) TÃĒn: %1$s @@ -171,9 +172,10 @@ Không tháģƒ làm máģ›i thông bÃĄo Không táēŖi đưáģŖc thông bÃĄo ÄÃŖ cÃŗ điáģƒm cáģ§a %1$s - Bài táē­p Ä‘ÃŖ cÃŗ điáģƒm - Ngày đáēŋn háēĄn Ä‘ÃŖ thay đáģ•i - Tráģng sáģ‘ Ä‘iáģƒm Ä‘ÃŖ thay dáģ•i + Điáģƒm Ä‘ÃŖ đưáģŖc thay đáģ•i + Ngày đáēŋn háēĄn + Điáģƒm + Thông BÃĄo Thông bÃĄo táģĢ %1$s Tráģng sáģ‘ Ä‘iáģƒm cáģ§a %1$s Ä‘ÃŖ thay đáģ•i %1$s đáēŋn háēĄn vào %2$s @@ -346,4 +348,35 @@ TáēĨt CáēŖ Máģ™t pháē§n cáģ§a %s Không tháģƒ máģŸ khÃŗa háģc. + Máģ™t pháē§n cáģ§a %s + Chào máģĢng! Xem chÆ°ÆĄng trÃŦnh cáģ§a báēĄn đáģƒ ghi danh vào khÃŗa háģc đáē§u tiÃĒn. + ChÆ°ÆĄng trÃŦnh cáģ§a tôi + Ghi danh vào khÃŗa háģc không thành công. + TáēŖi cÃĄc chÆ°ÆĄng trÃŦnh và khÃŗa háģc không thành công. + Làm máģ›i cÃĄc chÆ°ÆĄng trÃŦnh và khÃŗa háģc không thành công. + Hôm Nay + Hôm qua + Không tháģƒ máģŸ Thông BÃĄo + Không cÃŗ công cáģĨ ngoài nào cho khÃŗa háģc này. + Sáģ• Tay + Thông BÃĄo + Háģ™p Thư Đáēŋn + Tháēģ KhÃŗa Háģc %1$d / %2$d + %1$d\%% hoàn thành + ChÃēng tôi không tháģƒ táēŖi náģ™i dung này. + Vui lÃ˛ng tháģ­ láēĄi. + Tháģ­ LáēĄi + Chào máģĢng! Xem chÆ°ÆĄng trÃŦnh cáģ§a báēĄn đáģƒ ghi danh vào khÃŗa háģc đáē§u tiÃĒn. + Chi tiáēŋt chÆ°ÆĄng trÃŦnh + ChÃēc máģĢng! BáēĄn Ä‘ÃŖ hoàn thành khÃŗa háģc. Xem tiáēŋn trÃŦnh và điáģƒm sáģ‘ cáģ§a báēĄn trÃĒn trang Háģc Táē­p. + Cháģn KhÃŗa Háģc + ÄÃŗng + ÄÃŖ MáģŸ Ráģ™ng + ÄÃŖ Thu Gáģn + MáģŸ Ráģ™ng + Thu Gáģn + ÄÃŖ hoàn thành + Chưa hoàn thành + Báģ‹ KhÃŗa + ÄÃŖ Háģ§y Cháģn diff --git a/libs/horizon/src/main/res/values-zh/strings.xml b/libs/horizon/src/main/res/values-zh/strings.xml index d53ac1b060..8c70a7e1a4 100644 --- a/libs/horizon/src/main/res/values-zh/strings.xml +++ b/libs/horizon/src/main/res/values-zh/strings.xml @@ -66,6 +66,7 @@ æ€ģ览 垗分、分数 å¤‡æŗ¨ + åˇĨå…ˇ äŊœä¸šåį§° (A-Z) æˆĒæ­ĸæ—Ĩ期īŧˆäģŽæ–°åˆ°æ—§īŧ‰ åį§°īŧš%1$s @@ -167,9 +168,10 @@ åˆˇæ–°é€šįŸĨå¤ąč´Ĩ 加čŊŊ通įŸĨå¤ąč´Ĩ įŽ°åœ¨å¯äģĨæŸĨįœ‹ %1$s įš„č¯„åˆ† - äŊœä¸šåˇ˛č¯„分 - æˆĒæ­ĸæ—ĨæœŸåˇ˛æ›´æ”š - č¯„åˆ†æƒé‡åˇ˛æ›´æ”š + åˆ†æ•°åˇ˛æ›´æ”š + æˆĒæ­ĸæ—Ĩ期 + č¯„åˆ† + å…Ŧ告 æĨč‡Ē %1$s įš„å…Ŧ告 %1$s įš„č¯„åˆ†æƒé‡åˇ˛æ›´æ”š %1$s įš„æˆĒæ­ĸæ—Ĩ期ä¸ē %2$s @@ -340,4 +342,35 @@ 全部 %s įš„ä¸€éƒ¨åˆ† æ— æŗ•æ‰“åŧ€č¯žį¨‹ã€‚ + %s įš„ä¸€éƒ¨åˆ† + æŦĸčŋŽīŧæŸĨįœ‹æ‚¨įš„čŽĄåˆ’äģĨæŗ¨å†ŒįŦŦ一ä¸Ēč¯žį¨‹ã€‚ + æˆ‘įš„čŽĄåˆ’ + æŗ¨å†Œč¯žį¨‹å¤ąč´Ĩ。 + 加čŊŊčŽĄåˆ’å’Œč¯žį¨‹å¤ąč´Ĩ。 + åˆˇæ–°čŽĄåˆ’å’Œč¯žį¨‹å¤ąč´Ĩ。 + äģŠå¤Š + 昨夊 + æ— æŗ•æ‰“åŧ€é€šįŸĨ + æ˛Ąæœ‰į”¨äēŽæ­¤č¯žį¨‹įš„外部åˇĨå…ˇã€‚ + įŦ”čްæœŦ + 通įŸĨ + æ”ļäģļįŽą + č¯žį¨‹åĄį‰‡ %1$d / %2$d + %1$d\%% 厌成 + æ— æŗ•åŠ čŊŊ此内厚。 + č¯ˇé‡č¯•ã€‚ + é‡č¯• + æŦĸčŋŽīŧæŸĨįœ‹æ‚¨įš„čŽĄåˆ’äģĨæŗ¨å†ŒįŦŦ一ä¸Ēč¯žį¨‹ + čŽĄåˆ’č¯Ļ情 + įĨč´ēīŧæ‚¨åˇ˛åŽŒæˆč¯žį¨‹ã€‚č¯ˇåœ¨å­Ļäš éĄĩéĸ上æŸĨįœ‹čŋ›åēĻå’Œč¯„åˆ†ã€‚ + é€‰æ‹Šč¯žį¨‹ + å…ŗé—­ + åˇ˛æ‰Šåą• + 折叠 + åą•åŧ€ + 折叠 + åˇ˛åŽŒæˆ + æœĒ厌成 + åˇ˛é”åŽš + æœĒ选中 diff --git a/libs/horizon/src/main/res/values/strings.xml b/libs/horizon/src/main/res/values/strings.xml index f4000d422d..3e1308d2e4 100644 --- a/libs/horizon/src/main/res/values/strings.xml +++ b/libs/horizon/src/main/res/values/strings.xml @@ -171,9 +171,10 @@ Failed to refresh notifications Failed to load notifications %1$s\'s score is now available - Assignment scored - Due date changed - Scoring weight changed + Score changed + Due date + Score + Announcement Announcement from %1$s %1$s\'s score weight was changed %1$s is due on %2$s @@ -352,5 +353,91 @@ Failed to enroll in course. Failed to load programs and courses. Failed to refresh programs and courses. + Today + Yesterday + Unable to open Notification There are no external tools for this course. + Notebook + Notifications + Inbox + Course Card %1$d of %2$d + %1$d\%% complete + We weren’t able to load this content. + Please try again. + Refresh + Program details + Time learning + This widget will update once data becomes available. + We weren\'t able to load this content.\nPlease try again. + Refresh + hours + hours in your course + total + Skill Highlights + No data yet + This widget will update once data becomes available. + We weren\'t able to load this content. + Please try again. + Refresh + Beginner + Proficient + Advanced + Expert + Congrats! You\'ve completed your course. View your progress and scores on the Learn page. + Activities + completed + This widget will update once data becomes available. + We weren\'t able to load this content.\nPlease try again. + Refresh + Skills + earned + This widget will update once data becomes available. + Refresh + We weren\'t able to load this content.\nPlease try again. + Select Course + Close + Expanded + Collapsed + Expand + Collapse + Completed + Not completed + Locked + Unselected + From %1$s + Go to announcement + We weren\'t able to load this content.\nPlease try again. + %1$d of %2$d + Previous announcement + Next announcement + Program + Welcome to %1$s! View your program to enroll in your first course. + You aren’t currently enrolled in a course. + %1$s widget is loading + Courses + %1$d completed + %1$d earned + %1$d hours in %2$s + %1$d %2$s in %3$s + %1$d hours and %2$d minutes in %3$s + + hour + hours + + + hr + hrs + + + minute + minutes + + + min + mins + + Refresh + Next item + Previous item + %1$d of %2$d \ No newline at end of file diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/account/AccountRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/account/AccountRepositoryTest.kt new file mode 100644 index 0000000000..0854a642e0 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/account/AccountRepositoryTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.account + +import com.instructure.canvasapi2.apis.ExperienceAPI +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.canvasapi2.models.ExperienceSummary +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class AccountRepositoryTest { + private val userApi: UserAPI.UsersInterface = mockk(relaxed = true) + private val experienceAPI: ExperienceAPI = mockk(relaxed = true) + + @Test + fun `Test successful user details retrieval`() = runTest { + val user = User(id = 1L, name = "Test User", email = "test@example.com") + coEvery { userApi.getSelf(any()) } returns DataResult.Success(user) + + val result = getRepository().getUserDetails(false) + + assertEquals(user, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed user details retrieval throws exception`() = runTest { + coEvery { userApi.getSelf(any()) } returns DataResult.Fail() + + getRepository().getUserDetails(false) + } + + @Test + fun `Test successful experiences retrieval`() = runTest { + val experiences = ExperienceSummary(availableApps = listOf("app1", "app2")) + coEvery { experienceAPI.getExperienceSummary(any()) } returns DataResult.Success(experiences) + + val result = getRepository().getExperiences(false) + + assertEquals(2, result.size) + assertTrue(result.contains("app1")) + assertTrue(result.contains("app2")) + } + + @Test + fun `Test failed experiences retrieval returns empty list`() = runTest { + coEvery { experienceAPI.getExperienceSummary(any()) } returns DataResult.Fail() + + val result = getRepository().getExperiences(false) + + assertTrue(result.isEmpty()) + } + + private fun getRepository(): AccountRepository { + return AccountRepository(userApi, experienceAPI) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/account/AccountViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/account/AccountViewModelTest.kt new file mode 100644 index 0000000000..133e2b423b --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/account/AccountViewModelTest.kt @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.account + +import android.content.Context +import com.instructure.canvasapi2.models.ExperienceSummary +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.features.reminder.AlarmScheduler +import com.instructure.pandautils.room.offline.DatabaseProvider +import com.instructure.pandautils.utils.LogoutHelper +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AccountViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: AccountRepository = mockk(relaxed = true) + private val logoutHelper: LogoutHelper = mockk(relaxed = true) + private val databaseProvider: DatabaseProvider = mockk(relaxed = true) + private val alarmScheduler: AlarmScheduler = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val testUser = User( + id = 1L, + name = "Test User", + shortName = "TUser" + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + coEvery { repository.getUserDetails(any()) } returns testUser + coEvery { repository.getExperiences(any()) } returns listOf() + every { context.getString(any()) } returns "Test String" + every { context.getString(any(), any()) } returns "Test String" + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test data loads successfully on init`() = runTest { + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.screenState.isLoading) + coVerify { repository.getUserDetails(false) } + } + + @Test + fun `Test user name is set from repository`() = runTest { + val viewModel = getViewModel() + + assertEquals(testUser.shortName, viewModel.uiState.value.userName) + } + + @Test + fun `Test user name falls back to name if shortName is null`() = runTest { + val userWithoutShortName = testUser.copy(shortName = null) + coEvery { repository.getUserDetails(any()) } returns userWithoutShortName + + val viewModel = getViewModel() + + assertEquals(testUser.name, viewModel.uiState.value.userName) + } + + @Test + fun `Test failed data load sets error state`() = runTest { + coEvery { repository.getUserDetails(any()) } throws Exception("Network error") + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.screenState.isLoading) + assertTrue(viewModel.uiState.value.screenState.isError) + } + + @Test + fun `Test experience switcher is shown when academic experience is available`() = runTest { + coEvery { repository.getExperiences(any()) } returns listOf(ExperienceSummary.ACADEMIC_EXPERIENCE) + + val viewModel = getViewModel() + + val hasExperienceGroup = viewModel.uiState.value.accountGroups.any { group -> + group.items.any { it.type is AccountItemType.SwitchExperience } + } + assertTrue(hasExperienceGroup) + } + + @Test + fun `Test experience switcher is hidden when academic experience is not available`() = runTest { + coEvery { repository.getExperiences(any()) } returns listOf() + + val viewModel = getViewModel() + + val hasExperienceGroup = viewModel.uiState.value.accountGroups.any { group -> + group.items.any { it.type is AccountItemType.SwitchExperience } + } + assertFalse(hasExperienceGroup) + } + + @Test + fun `Test account groups are initialized`() = runTest { + val viewModel = getViewModel() + + assertTrue(viewModel.uiState.value.accountGroups.isNotEmpty()) + } + + @Test + fun `Test settings group contains expected items`() = runTest { + val viewModel = getViewModel() + + val settingsGroup = viewModel.uiState.value.accountGroups.firstOrNull { group -> + group.items.any { it.type is AccountItemType.Open } + } + + assertTrue(settingsGroup != null) + assertTrue(settingsGroup!!.items.isNotEmpty()) + } + + @Test + fun `Test logout group is present`() = runTest { + val viewModel = getViewModel() + + val logoutGroup = viewModel.uiState.value.accountGroups.firstOrNull { group -> + group.items.any { it.type is AccountItemType.LogOut } + } + + assertTrue(logoutGroup != null) + } + + @Test + fun `Test update user name changes state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateUserName("New Name") + + assertEquals("New Name", viewModel.uiState.value.userName) + } + + @Test + fun `Test perform logout calls logout helper`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.performLogout() + + verify { logoutHelper.logout(databaseProvider, alarmScheduler) } + } + + @Test + fun `Test switch experience sets restart flag`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.switchExperience() + + assertTrue(viewModel.uiState.value.restartApp) + verify { apiPrefs.canvasCareerView = false } + } + + @Test + fun `Test experiences are fetched`() = runTest { + val viewModel = getViewModel() + + coVerify { repository.getExperiences(false) } + } + + private fun getViewModel(): AccountViewModel { + return AccountViewModel( + context, + repository, + logoutHelper, + databaseProvider, + alarmScheduler, + apiPrefs + ) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatRepositoryTest.kt new file mode 100644 index 0000000000..2e7f30db3e --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatRepositoryTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.aiassistant.chat + +import com.instructure.canvasapi2.managers.graphql.horizon.cedar.CedarApiManager +import com.instructure.canvasapi2.managers.graphql.horizon.pine.DocumentSource +import com.instructure.canvasapi2.managers.graphql.horizon.pine.PineApiManager +import com.instructure.pine.type.MessageInput +import com.instructure.pine.type.Role +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class AiAssistChatRepositoryTest { + private val cedarApi: CedarApiManager = mockk(relaxed = true) + private val pineApi: PineApiManager = mockk(relaxed = true) + + @Test + fun `Test answer prompt without context`() = runTest { + val prompt = "What is 2+2?" + val expectedResponse = "4" + + coEvery { cedarApi.answerPrompt(prompt, null) } returns expectedResponse + + val result = getRepository().answerPrompt(prompt) + + assertEquals(expectedResponse, result) + coVerify { cedarApi.answerPrompt(prompt, null) } + } + + @Test + fun `Test answer prompt with context`() = runTest { + val prompt = "Summarize this content" + val context = "This is test content to summarize" + val expectedResponse = "Summary of test content" + + coEvery { cedarApi.answerPrompt(any(), any()) } returns expectedResponse + + val result = getRepository().answerPrompt(prompt, context) + + assertEquals(expectedResponse, result) + coVerify { cedarApi.answerPrompt(prompt, match { it != null }) } + } + + @Test + fun `Test answer prompt with messages and context map`() = runTest { + val messages = listOf( + MessageInput(role = Role.User, text = "Test message") + ) + val context = mapOf("courseId" to "123") + val expectedResponse = "AI response" + + coEvery { pineApi.queryDocument(messages, DocumentSource.canvas, context) } returns expectedResponse + + val result = getRepository().answerPrompt(messages, context) + + assertEquals(expectedResponse, result) + } + + @Test + fun `Test summarize prompt with default paragraphs`() = runTest { + val contextString = "Long content to summarize..." + val summaryParagraphs = listOf("Summary paragraph 1", "Summary paragraph 2") + val expectedResponse = summaryParagraphs.joinToString("\n") + + coEvery { cedarApi.summarizeContent(contextString, 1) } returns summaryParagraphs + + val result = getRepository().summarizePrompt(contextString) + + assertEquals(expectedResponse, result) + coVerify { cedarApi.summarizeContent(contextString, 1) } + } + + @Test + fun `Test summarize prompt with custom paragraph count`() = runTest { + val contextString = "Long content to summarize..." + val numberOfParagraphs = 3 + val summaryParagraphs = listOf("Para 1", "Para 2", "Para 3") + val expectedResponse = summaryParagraphs.joinToString("\n") + + coEvery { cedarApi.summarizeContent(contextString, numberOfParagraphs) } returns summaryParagraphs + + val result = getRepository().summarizePrompt(contextString, numberOfParagraphs) + + assertEquals(expectedResponse, result) + coVerify { cedarApi.summarizeContent(contextString, numberOfParagraphs) } + } + + @Test + fun `Test empty summary returns empty string`() = runTest { + val contextString = "Content" + coEvery { cedarApi.summarizeContent(contextString, 1) } returns emptyList() + + val result = getRepository().summarizePrompt(contextString) + + assertEquals("", result) + } + + @Test + fun `Test prompt with empty context string`() = runTest { + val prompt = "Test prompt" + val expectedResponse = "Response" + + coEvery { cedarApi.answerPrompt(prompt, null) } returns expectedResponse + + val result = getRepository().answerPrompt(prompt, null) + + assertEquals(expectedResponse, result) + } + + private fun getRepository(): AiAssistChatRepository { + return AiAssistChatRepository(cedarApi, pineApi) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModelTest.kt new file mode 100644 index 0000000000..f56f94a89e --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/aiassistant/chat/AiAssistChatViewModelTest.kt @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.aiassistant.chat + +import android.content.Context +import androidx.compose.ui.text.input.TextFieldValue +import com.instructure.horizon.features.aiassistant.common.AiAssistContextProvider +import com.instructure.horizon.features.aiassistant.common.model.AiAssistContext +import com.instructure.horizon.features.aiassistant.common.model.AiAssistMessage +import com.instructure.horizon.features.aiassistant.common.model.AiAssistMessagePrompt +import com.instructure.horizon.features.aiassistant.common.model.AiAssistMessageRole +import com.instructure.pine.type.MessageInput +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AiAssistChatViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: AiAssistChatRepository = mockk(relaxed = true) + private val aiAssistContextProvider: AiAssistContextProvider = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val testContext = AiAssistContext( + contextString = "Test context", + contextSources = emptyList(), + chatHistory = mutableListOf() + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + every { aiAssistContextProvider.aiAssistContext } returns testContext + coEvery { repository.answerPrompt(any(), any()) } returns "Test response" + coEvery { repository.answerPrompt(any>(), any>()) } returns "Test response" + coEvery { repository.summarizePrompt(any()) } returns "Summary response" + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel initializes with empty chat history`() = runTest { + val viewModel = getViewModel() + + assertTrue(viewModel.uiState.value.messages.isEmpty()) + assertFalse(viewModel.uiState.value.isLoading) + } + + @Test + fun `Test ViewModel initializes with existing chat history`() = runTest { + val existingMessage = AiAssistMessage( + prompt = AiAssistMessagePrompt.Summarize, + role = AiAssistMessageRole.User + ) + val contextWithHistory = testContext.copy(chatHistory = mutableListOf(existingMessage)) + every { aiAssistContextProvider.aiAssistContext } returns contextWithHistory + + val viewModel = getViewModel() + + assertEquals(2, viewModel.uiState.value.messages.size) + assertFalse(viewModel.uiState.value.isLoading) + } + + @Test + fun `Test text input change updates state`() = runTest { + val viewModel = getViewModel() + + val newText = TextFieldValue("Test input") + viewModel.uiState.value.onInputTextChanged(newText) + + assertEquals("Test input", viewModel.uiState.value.inputTextValue.text) + } + + @Test + fun `Test text submission sends message`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onInputTextChanged(TextFieldValue("Test message")) + viewModel.uiState.value.onInputTextSubmitted() + testDispatcher.scheduler.advanceUntilIdle() + + assertTrue(viewModel.uiState.value.messages.any { + it.role == AiAssistMessageRole.User && + it.prompt is AiAssistMessagePrompt.Custom && + (it.prompt as AiAssistMessagePrompt.Custom).message == "Test message" + }) + } + + @Test + fun `Test text submission receives response`() = runTest { + val viewModel = getViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + viewModel.uiState.value.onInputTextChanged(TextFieldValue("Test message")) + viewModel.uiState.value.onInputTextSubmitted() + testDispatcher.scheduler.advanceUntilIdle() + + assertTrue(viewModel.uiState.value.messages.any { + it.role == AiAssistMessageRole.Assistant + }) + assertFalse(viewModel.uiState.value.isLoading) + } + + @Test + fun `Test text submission clears input field`() = runTest { + val viewModel = getViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + viewModel.uiState.value.onInputTextChanged(TextFieldValue("Test message")) + viewModel.uiState.value.onInputTextSubmitted() + testDispatcher.scheduler.advanceUntilIdle() + + assertEquals("", viewModel.uiState.value.inputTextValue.text) + } + + @Test + fun `Test existing summarize prompt is executed`() = runTest { + val existingMessage = AiAssistMessage( + prompt = AiAssistMessagePrompt.Summarize, + role = AiAssistMessageRole.User + ) + val contextWithHistory = testContext.copy(chatHistory = mutableListOf(existingMessage)) + every { aiAssistContextProvider.aiAssistContext } returns contextWithHistory + + val viewModel = getViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + coVerify { repository.summarizePrompt(any()) } + assertTrue(viewModel.uiState.value.messages.size >= 2) + } + + @Test + fun `Test custom prompt calls answer prompt`() = runTest { + val viewModel = getViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + viewModel.uiState.value.onInputTextChanged(TextFieldValue("Custom question")) + viewModel.uiState.value.onInputTextSubmitted() + testDispatcher.scheduler.advanceUntilIdle() + + coVerify { repository.answerPrompt("Custom question", any()) } + } + + @Test + fun `Test loading state is set during message submission`() = runTest { + coEvery { repository.answerPrompt(any(), any()) } coAnswers { + // Simulate a delay + kotlinx.coroutines.delay(100) + "Response" + } + + val viewModel = getViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + viewModel.uiState.value.onInputTextChanged(TextFieldValue("Test message")) + viewModel.uiState.value.onInputTextSubmitted() + + // The loading state should be set immediately + assertTrue(viewModel.uiState.value.isLoading) + + testDispatcher.scheduler.advanceUntilIdle() + + // After completion, loading should be false + assertFalse(viewModel.uiState.value.isLoading) + } + + @Test + fun `Test AI context is passed to UI state`() = runTest { + val viewModel = getViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + assertEquals(testContext, viewModel.uiState.value.aiContext) + } + + @Test + fun `Test messages are appended in correct order`() = runTest { + val viewModel = getViewModel() + testDispatcher.scheduler.advanceUntilIdle() + + viewModel.uiState.value.onInputTextChanged(TextFieldValue("First message")) + viewModel.uiState.value.onInputTextSubmitted() + testDispatcher.scheduler.advanceUntilIdle() + + viewModel.uiState.value.onInputTextChanged(TextFieldValue("Second message")) + viewModel.uiState.value.onInputTextSubmitted() + testDispatcher.scheduler.advanceUntilIdle() + + val messages = viewModel.uiState.value.messages + assertTrue(messages.size >= 4) // 2 user messages + 2 assistant responses + + // Check that messages alternate between user and assistant + val userMessages = messages.filterIndexed { index, _ -> index % 2 == 0 } + assertTrue(userMessages.all { it.role == AiAssistMessageRole.User }) + } + + private fun getViewModel(): AiAssistChatViewModel { + return AiAssistChatViewModel(context, repository, aiAssistContextProvider) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/DashboardRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/DashboardRepositoryTest.kt new file mode 100644 index 0000000000..9d90d80dce --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/DashboardRepositoryTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard + +import com.instructure.canvasapi2.apis.UnreadCountAPI +import com.instructure.canvasapi2.models.UnreadNotificationCount +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class DashboardRepositoryTest { + private val unreadCountApi: UnreadCountAPI.UnreadCountsInterface = mockk(relaxed = true) + + private val notificationCounts = listOf( + UnreadNotificationCount( + type = "Message", + count = 5, + unreadCount = 10, + ), + UnreadNotificationCount( + type = "Conversation", + count = 2, + unreadCount = 5, + ), + UnreadNotificationCount( + type = "Announcement", + count = 1, + unreadCount = 3, + ), + ) + + @Before + fun setup() { + coEvery { unreadCountApi.getNotificationsCount(any()) } returns DataResult.Success(notificationCounts) + } + + @Test + fun `Test successful UnreadCount call`() = runTest { + val repository = getRepository() + val result = repository.getUnreadCounts(true) + assertEquals(3, result.size) + assertEquals(notificationCounts, result) + } + + private fun getRepository(): DashboardRepository { + return DashboardRepository(unreadCountApi) + } +} \ No newline at end of file diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/DashboardViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/DashboardViewModelTest.kt new file mode 100644 index 0000000000..d782bef568 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/DashboardViewModelTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard + +import com.instructure.canvasapi2.models.UnreadNotificationCount +import com.instructure.pandautils.utils.ThemePrefs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class DashboardViewModelTest { + private val repository: DashboardRepository = mockk(relaxed = true) + private val themePrefs: ThemePrefs = mockk(relaxed = true) + private val dashboardEventHandler: DashboardEventHandler = DashboardEventHandler() + private val testDispatcher = UnconfinedTestDispatcher() + + private val notificationCounts = listOf( + UnreadNotificationCount( + type = "Message", + count = 5, + unreadCount = 10, + ), + UnreadNotificationCount( + type = "Conversation", + count = 2, + unreadCount = 5, + ), + UnreadNotificationCount( + type = "Announcement", + count = 1, + unreadCount = 3, + ), + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + coEvery { repository.getUnreadCounts(any()) } returns notificationCounts + coEvery { themePrefs.mobileLogoUrl } returns "" + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel successfully loads and filters unread counts`() { + val viewModel = getViewModel() + coVerify { repository.getUnreadCounts(true) } + + val state = viewModel.uiState.value + assertEquals(5, state.unreadCountState.unreadConversations) + assertEquals(10, state.unreadCountState.unreadNotifications) + } + + @Test + fun `Test ViewModel loads logo URL from ThemePrefs`() { + val logoUrl = "https://example.com/logo.png" + coEvery { themePrefs.mobileLogoUrl } returns logoUrl + + val viewModel = getViewModel() + val state = viewModel.uiState.value + assertEquals(logoUrl, state.logoUrl) + } + + @Test + fun `Refresh event triggers refresh and updates unread counts`() = runTest { + val viewModel = getViewModel() + + val updatedCounts = listOf( + UnreadNotificationCount( + type = "Message", + count = 8, + unreadCount = 15, + ), + UnreadNotificationCount( + type = "Conversation", + count = 3, + unreadCount = 8, + ), + ) + coEvery { repository.getUnreadCounts(any()) } returns updatedCounts + + dashboardEventHandler.postEvent(DashboardEvent.DashboardRefresh) + testScheduler.advanceUntilIdle() + + coVerify(atLeast = 2) { repository.getUnreadCounts(true) } + val state = viewModel.uiState.value + assertEquals(8, state.unreadCountState.unreadConversations) + assertEquals(15, state.unreadCountState.unreadNotifications) + } + + @Test + fun `ShowSnackbar event updates snackbar message in UI state`() = runTest { + val viewModel = getViewModel() + + val testMessage = "Test snackbar message" + dashboardEventHandler.postEvent(DashboardEvent.ShowSnackbar(testMessage)) + testScheduler.advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(testMessage, state.snackbarMessage) + } + + @Test + fun `Multiple refresh events update state correctly`() = runTest { + val viewModel = getViewModel() + + dashboardEventHandler.postEvent(DashboardEvent.DashboardRefresh) + testScheduler.advanceUntilIdle() + + val secondCounts = listOf( + UnreadNotificationCount(type = "Conversation", count = 10, unreadCount = 20) + ) + coEvery { repository.getUnreadCounts(any()) } returns secondCounts + + dashboardEventHandler.postEvent(DashboardEvent.DashboardRefresh) + testScheduler.advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(20, state.unreadCountState.unreadConversations) + } + + private fun getViewModel(): DashboardViewModel { + return DashboardViewModel(repository, themePrefs, dashboardEventHandler) + } +} \ No newline at end of file diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/course/DashboardCourseRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/course/DashboardCourseRepositoryTest.kt new file mode 100644 index 0000000000..108ceadccb --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/course/DashboardCourseRepositoryTest.kt @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.course + +import com.instructure.canvasapi2.GetCoursesQuery +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetProgramsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Program +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.type.EnrollmentWorkflowState +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.horizon.features.dashboard.widget.course.DashboardCourseRepository +import com.instructure.journey.type.ProgramVariantType +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class DashboardCourseRepositoryTest { + private val horizonGetCoursesManager: HorizonGetCoursesManager = mockk(relaxed = true) + private val moduleApi: ModuleAPI. ModuleInterface = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val enrollmentApi: EnrollmentAPI. EnrollmentInterface = mockk(relaxed = true) + private val getProgramsManager: GetProgramsManager = mockk(relaxed = true) + + private val userId = 1L + @Before + fun setup() { + every { apiPrefs.user?.id } returns userId + } + + @Test + fun `Test successful getEnrollments call`() = runTest { + val enrollments = listOf( + GetCoursesQuery.Enrollment( + "1", + EnrollmentWorkflowState.active, + null, + null + ) + ) + coEvery { horizonGetCoursesManager.getEnrollments(any(), any()) } returns DataResult.Success(enrollments) + val repository = getRepository() + + + val result = repository.getEnrollments(forceNetwork = true) + coVerify { horizonGetCoursesManager.getEnrollments(userId, true) } + assertEquals(enrollments, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed getEnrollments call`() = runTest { + coEvery { horizonGetCoursesManager.getEnrollments(any(), any()) } returns DataResult.Fail() + val repository = getRepository() + + repository.getEnrollments(forceNetwork = true) + coVerify { horizonGetCoursesManager.getEnrollments(userId, true) } + } + + @Test + fun `Test successful acceptInvite call`() = runTest { + val repository = getRepository() + coEvery { enrollmentApi.acceptInvite(any(), any(), any()) } returns DataResult.Success(Unit) + repository.acceptInvite(1, 1) + coVerify { enrollmentApi.acceptInvite(1, 1, any()) } + } + + @Test(expected = IllegalStateException::class) + fun `Test failed acceptInvite call`() = runTest { + val repository = getRepository() + coEvery { enrollmentApi.acceptInvite(any(), any(), any()) } returns DataResult.Fail() + repository.acceptInvite(1, 1) + coVerify { enrollmentApi.acceptInvite(1, 1, any()) } + } + + @Test + fun `Test successful getPrograms call`() = runTest { + val programs = listOf( + Program( + "1", + "Program 1", + null, + null, + null, + ProgramVariantType.LINEAR, + null, + emptyList() + ), + Program( + "2", + "Program 2", + null, + null, + null, + ProgramVariantType.NON_LINEAR, + null, + emptyList() + ), + ) + coEvery { getProgramsManager.getPrograms(any()) } returns programs + val repository = getRepository() + + val result = repository.getPrograms() + coVerify { getProgramsManager.getPrograms(any()) } + assertEquals(programs, result) + } + + @Test + fun `Test successful getFirstPageModulesWithItems call`() = runTest { + val courseId = 1L + val modules = listOf( + ModuleObject( + id = 1, + name = "Module 1", + items = listOf( + ModuleItem( + id = 1, + title = "Module Item 1", + moduleId = 1, + contentId = 1, + type = "Page", + estimatedDuration = "PT10M" + ) + ) + ), + ModuleObject( + id = 2, + name = "Module 2", + items = listOf( + ModuleItem( + id = 2, + title = "Module Item 2", + moduleId = 2, + contentId = 2, + type = "Assignment", + estimatedDuration = "PT10M" + ) + ) + ), + ) + coEvery { moduleApi.getFirstPageModulesWithItems(any(), any(), any(), any()) } returns DataResult.Success(modules) + val repository = getRepository() + + val result = repository.getFirstPageModulesWithItems(courseId, forceNetwork = true) + coVerify { moduleApi.getFirstPageModulesWithItems(CanvasContext.Type.COURSE.apiString, courseId, any(), listOf("estimated_durations")) } + assertEquals(modules, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed getFirstPageModulesWithItems call`() = runTest { + val courseId = 1L + coEvery { moduleApi.getFirstPageModulesWithItems(any(), any(), any(), any()) } returns DataResult.Fail() + val repository = getRepository() + + repository.getFirstPageModulesWithItems(courseId, forceNetwork = true) + coVerify { moduleApi.getFirstPageModulesWithItems(CanvasContext.Type.COURSE.apiString, courseId, any(), listOf("estimated_durations")) } + } + + private fun getRepository(): DashboardCourseRepository { + return DashboardCourseRepository( + horizonGetCoursesManager, + moduleApi, + apiPrefs, + enrollmentApi, + getProgramsManager + ) + } +} \ No newline at end of file diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/course/DashboardCourseViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/course/DashboardCourseViewModelTest.kt new file mode 100644 index 0000000000..9c34deee31 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/course/DashboardCourseViewModelTest.kt @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.course + +import android.content.Context +import com.instructure.canvasapi2.GetCoursesQuery +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Program +import com.instructure.canvasapi2.managers.graphql.horizon.journey.ProgramRequirement +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.type.EnrollmentWorkflowState +import com.instructure.horizon.features.dashboard.DashboardEventHandler +import com.instructure.horizon.features.dashboard.widget.course.DashboardCourseRepository +import com.instructure.horizon.features.dashboard.widget.course.DashboardCourseViewModel +import com.instructure.journey.type.ProgramProgressCourseEnrollmentStatus +import com.instructure.journey.type.ProgramVariantType +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class DashboardCourseViewModelTest { + private val context: Context = mockk(relaxed = true) + private var repository: DashboardCourseRepository = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + private val dashboardEventHandler = DashboardEventHandler() + + private val courses = listOf( + GetCoursesQuery.Course( + id = "1", + name = "Course 1", + image_download_url = "url_1", + syllabus_body = "syllabus 1", + account = GetCoursesQuery.Account("Account 1"), + usersConnection = null + ), + GetCoursesQuery.Course( + id = "2", + name = "Course 2", + image_download_url = null, + syllabus_body = null, + account = null, + usersConnection = null + ), + GetCoursesQuery.Course( + id = "3", + name = "Course 3", + image_download_url = null, + syllabus_body = null, + account = null, + usersConnection = null + ), + GetCoursesQuery.Course( + id = "4", + name = "Course 4", + image_download_url = null, + syllabus_body = null, + account = null, + usersConnection = null + ), + ) + private val activeEnrollments = listOf( + GetCoursesQuery.Enrollment( + id = "1", + state = EnrollmentWorkflowState.active, + lastActivityAt = Date(), + course = courses[0] + ), + GetCoursesQuery.Enrollment( + id = "2", + state = EnrollmentWorkflowState.active, + lastActivityAt = Date(), + course = courses[1] + ), + ) + private val invitedEnrollments = listOf( + GetCoursesQuery.Enrollment( + id = "3", + state = EnrollmentWorkflowState.invited, + lastActivityAt = Date(), + course = courses[2] + ) + ) + private val completedEnrollments = listOf( + GetCoursesQuery.Enrollment( + id = "4", + state = EnrollmentWorkflowState.completed, + lastActivityAt = Date(), + course = courses[3] + ) + ) + private val programs = listOf( + Program( // Not started Program + id = "1", + name = "Program 1", + description = "Program 1 description", + startDate = null, + endDate = null, + variant = ProgramVariantType.LINEAR, + sortedRequirements = emptyList() + ), + Program( // Program with Course 2 + id = "2", + name = "Program 2", + description = "Program 2 description", + startDate = null, + endDate = null, + variant = ProgramVariantType.LINEAR, + sortedRequirements = listOf( + ProgramRequirement( + id = "1", + progressId = "1", + courseId = 2, + required = true, + progress = 5.0, + enrollmentStatus = ProgramProgressCourseEnrollmentStatus.ENROLLED + ) + ) + ) + ) + private val modules = listOf( + ModuleObject( + id = 1, + name = "Module 1", + items = listOf( + ModuleItem( + id = 1, + title = " Module Item 1", + moduleId = 1, + contentId = 1, + type = "Page", + estimatedDuration = "PT11M" + ) + ) + ) + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + + coEvery { repository.getEnrollments(any()) } returns activeEnrollments + invitedEnrollments + completedEnrollments + coEvery { repository.getPrograms(any()) } returns programs + coEvery { repository.acceptInvite(any(), any()) } just runs + coEvery { repository.getFirstPageModulesWithItems(any(), any()) } returns modules + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test course and empty programs are in the state list`() { + coEvery { repository.getEnrollments(any()) } returns activeEnrollments + completedEnrollments + val viewModel = getViewModel() + val state = viewModel.uiState.value + assertEquals(3, state.courses.size) + assertTrue(state.courses.any { it.title == "Course 1" }) + assertTrue(state.courses.any { it.title == "Course 2" }) + assertTrue(state.courses.any { it.title == "Course 4" }) + assertTrue(state.courses.none { it.title == "Course 3" }) + + assertEquals(1, state.programs.items.size) + } + + @Test + fun `Test course invitations are automatically accepted`() { + val viewModel = getViewModel() + coVerify { repository.acceptInvite(3, 3) } + } + + private fun getViewModel(): DashboardCourseViewModel { + return DashboardCourseViewModel(context, repository, dashboardEventHandler) + } +} \ No newline at end of file diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerRepositoryTest.kt new file mode 100644 index 0000000000..4e8dcb0ea6 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerRepositoryTest.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.announcement + +import com.instructure.canvasapi2.apis.AccountNotificationAPI +import com.instructure.canvasapi2.apis.AnnouncementAPI +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.models.AccountNotification +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import java.util.Date + +class DashboardAnnouncementBannerRepositoryTest { + private val announcementApi: AnnouncementAPI.AnnouncementInterface = mockk(relaxed = true) + private val accountNotificationApi: AccountNotificationAPI.AccountNotificationInterface = mockk(relaxed = true) + private val getCoursesManager: HorizonGetCoursesManager = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val userId = 1L + + @Before + fun setup() { + coEvery { apiPrefs.user?.id } returns userId + } + + @Test + fun `Test successful unread course announcements retrieval`() = runTest { + val courses = listOf( + CourseWithProgress(courseId = 1L, courseName = "Course 1", progress = 50.0) + ) + val announcements = listOf( + createDiscussionTopicHeader("1", "Course Announcement 1", "course_1", false) + ) + + setupCourseMocks(courses, announcements) + coEvery { accountNotificationApi.getAccountNotifications(any(), any(), any()) } returns DataResult.Success(emptyList()) + + val result = getRepository().getUnreadAnnouncements(false) + + assertEquals(1, result.size) + assertEquals("Course Announcement 1", result[0].title) + assertEquals("Course 1", result[0].source) + assertEquals(AnnouncementType.COURSE, result[0].type) + } + + @Test + fun `Test filters out read announcements`() = runTest { + val courses = listOf(CourseWithProgress(courseId = 1L, courseName = "Course 1", progress = 10.0)) + val announcements = listOf( + createDiscussionTopicHeader("1", "Unread Announcement", "course_1", false), + createDiscussionTopicHeader("2", "Read Announcement", "course_1", true) + ) + + setupCourseMocks(courses, announcements) + coEvery { accountNotificationApi.getAccountNotifications(any(), any(), any()) } returns DataResult.Success(emptyList()) + + val result = getRepository().getUnreadAnnouncements(false) + + assertEquals(1, result.size) + assertEquals("Unread Announcement", result[0].title) + } + + @Test + fun `Test successful global announcements retrieval`() = runTest { + val globalAnnouncements = listOf( + AccountNotification(id = 1L, subject = "Global Announcement 1", closed = false), + AccountNotification(id = 2L, subject = "Global Announcement 2", closed = false) + ) + + setupCourseMocks(emptyList(), emptyList()) + coEvery { accountNotificationApi.getAccountNotifications(any(), any(), any()) } returns DataResult.Success(globalAnnouncements) + coEvery { accountNotificationApi.getNextPageNotifications(any(), any()) } returns DataResult.Fail() + + val result = getRepository().getUnreadAnnouncements(false) + + assertEquals(2, result.size) + assertEquals("Global Announcement 1", result[0].title) + assertEquals(null, result[0].source) + assertEquals(AnnouncementType.GLOBAL, result[0].type) + } + + @Test + fun `Test empty announcements list`() = runTest { + setupCourseMocks(emptyList(), emptyList()) + coEvery { accountNotificationApi.getAccountNotifications(any(), any(), any()) } returns DataResult.Success(emptyList()) + coEvery { accountNotificationApi.getNextPageNotifications(any(), any()) } returns DataResult.Fail() + + val result = getRepository().getUnreadAnnouncements(false) + + assertEquals(0, result.size) + } + + @Test + fun `Test forceRefresh parameter is passed correctly`() = runTest { + val courses = listOf(CourseWithProgress(courseId = 1L, courseName = "Course 1", progress = 10.0)) + setupCourseMocks(courses, emptyList()) + coEvery { accountNotificationApi.getAccountNotifications(any(), any(), any()) } returns DataResult.Success(emptyList()) + coEvery { accountNotificationApi.getNextPageNotifications(any(), any()) } returns DataResult.Fail() + + getRepository().getUnreadAnnouncements(true) + + coVerify { getCoursesManager.getCoursesWithProgress(any(), true) } + coVerify { accountNotificationApi.getAccountNotifications(match { it.isForceReadFromNetwork }, any(), any()) } + } + + @Test + fun `Test empty courses list returns empty announcements`() = runTest { + coEvery { getCoursesManager.getCoursesWithProgress(any()) } returns DataResult.Success(emptyList()) + coEvery { accountNotificationApi.getAccountNotifications(any(), any(), any()) } returns DataResult.Success(emptyList()) + coEvery { accountNotificationApi.getNextPageNotifications(any(), any()) } returns DataResult.Fail() + + val result = getRepository().getUnreadAnnouncements(false) + + assertEquals(0, result.size) + } + + private fun createDiscussionTopicHeader( + id: String, + title: String, + contextCode: String, + isRead: Boolean, + postedDate: Date = Date() + ): DiscussionTopicHeader { + return DiscussionTopicHeader( + id = id.toLong(), + title = title, + contextCode = contextCode, + readState = if (isRead) "read" else "unread", + postedDate = postedDate, + htmlUrl = "test/url/$id" + ) + } + + private fun setupCourseMocks( + courses: List, + announcements: List + ) { + coEvery { getCoursesManager.getCoursesWithProgress(any(), any()) } returns DataResult.Success(courses) + coEvery { announcementApi.getFirstPageAnnouncements(any(), params = any()) } returns DataResult.Success(announcements) + coEvery { announcementApi.getNextPageAnnouncementsList(any(), any()) } returns DataResult.Success(announcements) + } + + private fun getRepository(): DashboardAnnouncementBannerRepository { + return DashboardAnnouncementBannerRepository( + announcementApi, + accountNotificationApi, + getCoursesManager, + apiPrefs + ) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerViewModelTest.kt new file mode 100644 index 0000000000..e787f2b8c2 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/announcement/DashboardAnnouncementBannerViewModelTest.kt @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.announcement + +import android.content.Context +import com.instructure.horizon.features.dashboard.DashboardEventHandler +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.DashboardPaginatedWidgetCardButtonRoute +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class DashboardAnnouncementBannerViewModelTest { + private val repository: DashboardAnnouncementBannerRepository = mockk(relaxed = true) + private val context: Context = mockk(relaxed = true) + private val eventHandler = DashboardEventHandler() + private val testDispatcher = UnconfinedTestDispatcher() + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test initialization loads announcement data`() = runTest { + val announcements = listOf( + AnnouncementBannerItem( + title = "Test Announcement", + source = "Course 101", + date = Date(), + type = AnnouncementType.COURSE, + route = "test/route" + ) + ) + coEvery { repository.getUnreadAnnouncements(false) } returns announcements + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.SUCCESS, state.state) + assertEquals(1, state.cardState.items.size) + assertEquals("Test Announcement", state.cardState.items[0].title) + coVerify { repository.getUnreadAnnouncements(false) } + } + + @Test + fun `Test shows all announcements`() = runTest { + val announcements = listOf( + AnnouncementBannerItem( + title = "First Announcement", + source = "Course 101", + date = Date(), + type = AnnouncementType.COURSE, + route = "test/route1" + ), + AnnouncementBannerItem( + title = "Second Announcement", + source = "Course 102", + date = Date(), + type = AnnouncementType.GLOBAL, + route = "test/route2" + ) + ) + coEvery { repository.getUnreadAnnouncements(false) } returns announcements + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(2, state.cardState.items.size) + assertEquals("First Announcement", state.cardState.items[0].title) + assertEquals("Second Announcement", state.cardState.items[1].title) + } + + @Test + fun `Test no announcements returns empty list`() = runTest { + coEvery { repository.getUnreadAnnouncements(false) } returns emptyList() + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.SUCCESS, state.state) + assertTrue(state.cardState.items.isEmpty()) + } + + @Test + fun `Test error state when repository throws exception`() = runTest { + coEvery { repository.getUnreadAnnouncements(false) } throws Exception("Network error") + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.ERROR, state.state) + } + + @Test + fun `Test refresh calls repository with forceNetwork true`() = runTest { + val announcements = listOf( + AnnouncementBannerItem( + title = "Test Announcement", + source = "Course 101", + date = Date(), + type = AnnouncementType.COURSE, + route = "test/route" + ) + ) + coEvery { repository.getUnreadAnnouncements(false) } returns announcements + coEvery { repository.getUnreadAnnouncements(true) } returns announcements + + val viewModel = getViewModel() + advanceUntilIdle() + + var completed = false + viewModel.uiState.value.onRefresh { completed = true } + advanceUntilIdle() + + assertTrue(completed) + coVerify { repository.getUnreadAnnouncements(true) } + } + + @Test + fun `Test refresh updates state to loading then success`() = runTest { + val announcements = listOf( + AnnouncementBannerItem( + title = "Test Announcement", + source = "Course 101", + date = Date(), + type = AnnouncementType.GLOBAL, + route = "test/route" + ) + ) + coEvery { repository.getUnreadAnnouncements(any()) } returns announcements + + val viewModel = getViewModel() + advanceUntilIdle() + + var completed = false + viewModel.uiState.value.onRefresh { completed = true } + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.SUCCESS, state.state) + assertTrue(completed) + } + + @Test + fun `Test refresh with error sets error state`() = runTest { + val announcements = listOf( + AnnouncementBannerItem( + title = "Test Announcement", + source = "Course 101", + date = Date(), + type = AnnouncementType.COURSE, + route = "test/route" + ) + ) + coEvery { repository.getUnreadAnnouncements(false) } returns announcements + coEvery { repository.getUnreadAnnouncements(true) } throws Exception("Refresh failed") + + val viewModel = getViewModel() + advanceUntilIdle() + + var completed = false + viewModel.uiState.value.onRefresh { completed = true } + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.ERROR, state.state) + assertTrue(completed) + } + + @Test + fun `Test course announcement type`() = runTest { + val announcements = listOf( + AnnouncementBannerItem( + title = "Course Announcement", + source = "Introduction to Kotlin", + date = Date(), + type = AnnouncementType.COURSE, + route = "courses/123/announcements/456" + ) + ) + coEvery { repository.getUnreadAnnouncements(false) } returns announcements + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(1, state.cardState.items.size) + assertEquals(DashboardPaginatedWidgetCardButtonRoute.MainRoute(announcements[0].route), state.cardState.items[0].route) + assertEquals("Introduction to Kotlin", state.cardState.items[0].source) + } + + @Test + fun `Test global announcement type`() = runTest { + val announcements = listOf( + AnnouncementBannerItem( + title = "Global Announcement", + source = null, + date = Date(), + type = AnnouncementType.GLOBAL, + route = "horizon/inbox/account_notification/123" + ) + ) + coEvery { repository.getUnreadAnnouncements(false) } returns announcements + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(1, state.cardState.items.size) + assertEquals(DashboardPaginatedWidgetCardButtonRoute.MainRoute(announcements[0].route), state.cardState.items[0].route) + assertEquals(null, state.cardState.items[0].source) + } + + private fun getViewModel(): DashboardAnnouncementBannerViewModel { + return DashboardAnnouncementBannerViewModel(repository, eventHandler, context) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressRepositoryTest.kt new file mode 100644 index 0000000000..a00800105c --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressRepositoryTest.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.myprogress + +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetWidgetsManager +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.journey.GetWidgetDataQuery +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +class DashboardMyProgressRepositoryTest { + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val getCoursesManager: HorizonGetCoursesManager = mockk(relaxed = true) + private val getWidgetsManager: GetWidgetsManager = mockk(relaxed = true) + + private lateinit var repository: DashboardMyProgressRepository + + private val userId = 1L + @Before + fun setup() { + every { apiPrefs.user?.id } returns userId + repository = DashboardMyProgressRepository(apiPrefs, getWidgetsManager, getCoursesManager) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `getLearningStatusData returns data successfully`() = runTest { + val widgetData = GetWidgetDataQuery.WidgetData( + lastModifiedDate = Date(), + data = listOf( + mapOf("module_count_completed" to 5) + ) + ) + + coEvery { getWidgetsManager.getLearningStatusWidgetData(null, false) } returns widgetData + + val result = repository.getLearningStatusData(null, false) + + assertEquals(1, result?.data?.size) + assertEquals(5, result?.data?.get(0)?.moduleCountCompleted) + coVerify { getWidgetsManager.getLearningStatusWidgetData(null, false) } + } + + @Test + fun `getLearningStatusData with courseId passes courseId to manager`() = runTest { + val courseId = 123L + val widgetData = GetWidgetDataQuery.WidgetData( + lastModifiedDate = Date(), + data = listOf( + mapOf("module_count_completed" to 10) + ) + ) + + coEvery { getWidgetsManager.getLearningStatusWidgetData(courseId, false) } returns widgetData + + val result = repository.getLearningStatusData(courseId, false) + + assertEquals(1, result?.data?.size) + assertEquals(10, result?.data?.get(0)?.moduleCountCompleted) + coVerify { getWidgetsManager.getLearningStatusWidgetData(courseId, false) } + } + + @Test + fun `getLearningStatusData with forceNetwork true uses network`() = runTest { + val widgetData = GetWidgetDataQuery.WidgetData( + lastModifiedDate = Date(), + data = listOf( + mapOf("module_count_completed" to 3) + ) + ) + + coEvery { getWidgetsManager.getLearningStatusWidgetData(null, true) } returns widgetData + + val result = repository.getLearningStatusData(null, true) + + assertEquals(1, result?.data?.size) + assertEquals(3, result?.data?.get(0)?.moduleCountCompleted) + coVerify { getWidgetsManager.getLearningStatusWidgetData(null, true) } + } + + @Test(expected = Exception::class) + fun `getLearningStatusData propagates exceptions`() = runTest { + coEvery { getWidgetsManager.getLearningStatusWidgetData(null, false) } throws Exception("Network error") + + repository.getLearningStatusData(null, false) + } + + @Test + fun `getLearningStatusData handles empty data list`() = runTest { + val widgetData = GetWidgetDataQuery.WidgetData( + lastModifiedDate = Date(), + data = emptyList() + ) + + coEvery { getWidgetsManager.getLearningStatusWidgetData(null, false) } returns widgetData + + val result = repository.getLearningStatusData(null, false) + + assertEquals(emptyList(), result?.data) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressViewModelTest.kt new file mode 100644 index 0000000000..3d87cb6d1c --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/myprogress/DashboardMyProgressViewModelTest.kt @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.myprogress + +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.horizon.features.dashboard.DashboardItemState +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class DashboardMyProgressViewModelTest { + + private val repository: DashboardMyProgressRepository = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private lateinit var viewModel: DashboardMyProgressViewModel + + @Before + fun setup() { + coEvery { repository.getCourses(any()) } returns listOf( + CourseWithProgress( + courseId = 1L, + courseName = "Course 1", + progress = 1.0 + ), + CourseWithProgress( + courseId = 2L, + courseName = "Course 2", + progress = 1.0 + ) + ) + Dispatchers.setMain(testDispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `init loads learning status data successfully`() = runTest { + val myProgressData = MyProgressWidgetData( + lastModifiedDate = Date(), + data = listOf( + MyProgressWidgetDataEntry( + courseId = 1L, + courseName = "Course 1", + userId = 1L, + userUUID = "uuid-1", + userName = "User 1", + userAvatarUrl = null, + userEmail = "user1@example.com", + moduleCountCompleted = 5, + moduleCountStarted = 2, + moduleCountLocked = 1, + moduleCountTotal = 8 + ) + ) + ) + + coEvery { repository.getLearningStatusData(courseId = null, forceNetwork = false) } returns myProgressData + + viewModel = DashboardMyProgressViewModel(repository) + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.SUCCESS, state.state) + assertEquals(5, state.cardState.moduleCountCompleted) + } + + @Test + fun `init handles error gracefully`() = runTest { + coEvery { repository.getLearningStatusData(courseId = null, forceNetwork = false) } throws Exception("Network error") + + viewModel = DashboardMyProgressViewModel(repository) + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.ERROR, state.state) + } + + @Test + fun `init handles module_count_completed from multiple entries`() = runTest { + val myProgressData = MyProgressWidgetData( + lastModifiedDate = Date(), + data = listOf( + MyProgressWidgetDataEntry( + courseId = 1L, + courseName = "Course 1", + userId = 1L, + userUUID = "uuid-1", + userName = "User 1", + userAvatarUrl = null, + userEmail = "user1@example.com", + moduleCountCompleted = 5, + moduleCountStarted = 2, + moduleCountLocked = 1, + moduleCountTotal = 8 + ), + MyProgressWidgetDataEntry( + courseId = 2L, + courseName = "Course 2", + userId = 1L, + userUUID = "uuid-1", + userName = "User 1", + userAvatarUrl = null, + userEmail = "user1@example.com", + moduleCountCompleted = 7, + moduleCountStarted = 3, + moduleCountLocked = 0, + moduleCountTotal = 10 + ) + ) + ) + + coEvery { repository.getLearningStatusData(courseId = null, forceNetwork = false) } returns myProgressData + + viewModel = DashboardMyProgressViewModel(repository) + + val state = viewModel.uiState.value + assertEquals(12, state.cardState.moduleCountCompleted) + } + + @Test + fun `init handles empty data`() = runTest { + val myProgressData = MyProgressWidgetData( + lastModifiedDate = Date(), + data = emptyList() + ) + + coEvery { repository.getLearningStatusData(courseId = null, forceNetwork = false) } returns myProgressData + + viewModel = DashboardMyProgressViewModel(repository) + + val state = viewModel.uiState.value + assertEquals(0, state.cardState.moduleCountCompleted) + } + + @Test + fun `init handles null data`() = runTest { + coEvery { repository.getLearningStatusData(courseId = null, forceNetwork = false) } returns null + + viewModel = DashboardMyProgressViewModel(repository) + + val state = viewModel.uiState.value + assertEquals(0, state.cardState.moduleCountCompleted) + } + + @Test + fun `refresh calls repository with forceNetwork true`() = runTest { + val myProgressData = MyProgressWidgetData( + lastModifiedDate = Date(), + data = listOf( + MyProgressWidgetDataEntry( + courseId = 1L, + courseName = "Course 1", + userId = 1L, + userUUID = "uuid-1", + userName = "User 1", + userAvatarUrl = null, + userEmail = "user1@example.com", + moduleCountCompleted = 10, + moduleCountStarted = 2, + moduleCountLocked = 1, + moduleCountTotal = 13 + ) + ) + ) + + coEvery { repository.getLearningStatusData(courseId = null, forceNetwork = any()) } returns myProgressData + + viewModel = DashboardMyProgressViewModel(repository) + + var refreshCompleted = false + viewModel.uiState.value.onRefresh { + refreshCompleted = true + } + + coVerify { repository.getLearningStatusData(courseId = null, forceNetwork = true) } + assertEquals(true, refreshCompleted) + } + + @Test + fun `refresh handles error and completes`() = runTest { + val myProgressData = MyProgressWidgetData( + lastModifiedDate = Date(), + data = listOf( + MyProgressWidgetDataEntry( + courseId = 1L, + courseName = "Course 1", + userId = 1L, + userUUID = "uuid-1", + userName = "User 1", + userAvatarUrl = null, + userEmail = "user1@example.com", + moduleCountCompleted = 10, + moduleCountStarted = 2, + moduleCountLocked = 1, + moduleCountTotal = 13 + ) + ) + ) + + coEvery { repository.getLearningStatusData(courseId = null, forceNetwork = false) } returns myProgressData + coEvery { repository.getLearningStatusData(courseId = null, forceNetwork = true) } throws Exception("Network error") + + viewModel = DashboardMyProgressViewModel(repository) + + var refreshCompleted = false + viewModel.uiState.value.onRefresh { + refreshCompleted = true + } + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.ERROR, state.state) + assertEquals(true, refreshCompleted) + } + + @Test + fun `refresh updates state to loading then success`() = runTest { + val myProgressData = MyProgressWidgetData( + lastModifiedDate = Date(), + data = listOf( + MyProgressWidgetDataEntry( + courseId = 1L, + courseName = "Course 1", + userId = 1L, + userUUID = "uuid-1", + userName = "User 1", + userAvatarUrl = null, + userEmail = "user1@example.com", + moduleCountCompleted = 10, + moduleCountStarted = 2, + moduleCountLocked = 1, + moduleCountTotal = 13 + ) + ) + ) + + coEvery { repository.getLearningStatusData(courseId = null, forceNetwork = any()) } returns myProgressData + + viewModel = DashboardMyProgressViewModel(repository) + + viewModel.uiState.value.onRefresh {} + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.SUCCESS, state.state) + } + + @Test + fun `filter widget data for courses that the user is enrolled in`() = runTest { + val myProgressData = MyProgressWidgetData( + lastModifiedDate = Date(), + data = listOf( + MyProgressWidgetDataEntry( + courseId = 1L, + courseName = "Course 1", + userId = 1L, + userUUID = "uuid-1", + userName = "User 1", + userAvatarUrl = null, + userEmail = "user1@example.com", + moduleCountCompleted = 5, + moduleCountStarted = 3, + moduleCountLocked = 2, + moduleCountTotal = 10 + ), + MyProgressWidgetDataEntry( + courseId = 3L, + courseName = "Course 3", + userId = 1L, + userUUID = "uuid-1", + userName = "User 1", + userAvatarUrl = null, + userEmail = "user1@example.com", + moduleCountCompleted = 3, + moduleCountStarted = 4, + moduleCountLocked = 5, + moduleCountTotal = 7 + ) + ) + ) + coEvery { repository.getLearningStatusData(courseId = null, forceNetwork = any()) } returns myProgressData + viewModel = DashboardMyProgressViewModel(repository) + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.SUCCESS, state.state) + assertEquals(5, state.cardState.moduleCountCompleted) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsRepositoryTest.kt new file mode 100644 index 0000000000..83b81e1666 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsRepositoryTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skillhighlights + +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetSkillsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Skill +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.util.Date + +class DashboardSkillHighlightsRepositoryTest { + private val getSkillsManager: GetSkillsManager = mockk(relaxed = true) + + @Test + fun `Test successful skills retrieval with completedOnly null`() = runTest { + val skills = listOf( + Skill("1", "Advanced Skill", "advanced", Date(), Date()), + Skill("2", "Beginner Skill", "beginner", Date(), Date()), + Skill("3", "Proficient Skill", "proficient", Date(), Date()) + ) + coEvery { getSkillsManager.getSkills(null, false) } returns skills + + val result = getRepository().getSkills(completedOnly = null, forceNetwork = false) + + assertEquals(3, result.size) + assertEquals(skills, result) + coVerify { getSkillsManager.getSkills(null, false) } + } + + @Test + fun `Test successful skills retrieval with completedOnly true`() = runTest { + val skills = listOf( + Skill("1", "Completed Skill", "expert", Date(), Date()) + ) + coEvery { getSkillsManager.getSkills(true, false) } returns skills + + val result = getRepository().getSkills(completedOnly = true, forceNetwork = false) + + assertEquals(1, result.size) + assertEquals(skills, result) + coVerify { getSkillsManager.getSkills(true, false) } + } + + @Test + fun `Test skills retrieval with forceNetwork true`() = runTest { + val skills = listOf( + Skill("1", "Network Skill", "advanced", Date(), Date()) + ) + coEvery { getSkillsManager.getSkills(null, true) } returns skills + + val result = getRepository().getSkills(completedOnly = null, forceNetwork = true) + + assertEquals(1, result.size) + coVerify { getSkillsManager.getSkills(null, true) } + } + + @Test + fun `Test empty skills list is returned correctly`() = runTest { + coEvery { getSkillsManager.getSkills(null, false) } returns emptyList() + + val result = getRepository().getSkills(completedOnly = null, forceNetwork = false) + + assertEquals(0, result.size) + } + + @Test + fun `Test skills with null proficiency level`() = runTest { + val skills = listOf( + Skill("1", "Skill Without Level", null, Date(), Date()) + ) + coEvery { getSkillsManager.getSkills(null, false) } returns skills + + val result = getRepository().getSkills(completedOnly = null, forceNetwork = false) + + assertEquals(1, result.size) + assertEquals(null, result[0].proficiencyLevel) + } + + private fun getRepository(): DashboardSkillHighlightsRepository { + return DashboardSkillHighlightsRepository(getSkillsManager) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsViewModelTest.kt new file mode 100644 index 0000000000..2aa9a85321 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/skillhighlights/DashboardSkillHighlightsViewModelTest.kt @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skillhighlights + +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Skill +import com.instructure.horizon.features.dashboard.DashboardItemState +import com.instructure.horizon.features.dashboard.widget.skillhighlights.card.SkillHighlightProficiencyLevel +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class DashboardSkillHighlightsViewModelTest { + private val repository: DashboardSkillHighlightsRepository = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test initialization loads skills data`() = runTest { + val skills = listOf( + Skill("1", "Advanced Skill", "advanced", Date(), Date()), + Skill("2", "Beginner Skill", "beginner", Date(), Date()), + Skill("3", "Proficient Skill", "proficient", Date(), Date()) + ) + coEvery { repository.getSkills(true, false) } returns skills + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.SUCCESS, state.state) + assertEquals(3, state.cardState.skills.size) + coVerify { repository.getSkills(true, false) } + } + + @Test + fun `Test skills are sorted by proficiency level then alphabetically`() = runTest { + val skills = listOf( + Skill("1", "Zebra Skill", "beginner", Date(), Date()), + Skill("2", "Apple Skill", "advanced", Date(), Date()), + Skill("3", "Banana Skill", "expert", Date(), Date()), + Skill("4", "Cherry Skill", "advanced", Date(), Date()) + ) + coEvery { repository.getSkills(true, false) } returns skills + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(3, state.cardState.skills.size) + // Should be: Banana (expert), Apple (advanced), Cherry (advanced) + assertEquals("Banana Skill", state.cardState.skills[0].name) + assertEquals(SkillHighlightProficiencyLevel.EXPERT, state.cardState.skills[0].proficiencyLevel) + assertEquals("Apple Skill", state.cardState.skills[1].name) + assertEquals(SkillHighlightProficiencyLevel.ADVANCED, state.cardState.skills[1].proficiencyLevel) + assertEquals("Cherry Skill", state.cardState.skills[2].name) + assertEquals(SkillHighlightProficiencyLevel.ADVANCED, state.cardState.skills[2].proficiencyLevel) + } + + @Test + fun `Test only top 3 skills are displayed`() = runTest { + val skills = listOf( + Skill("1", "Skill 1", "expert", Date(), Date()), + Skill("2", "Skill 2", "expert", Date(), Date()), + Skill("3", "Skill 3", "advanced", Date(), Date()), + Skill("4", "Skill 4", "advanced", Date(), Date()), + Skill("5", "Skill 5", "beginner", Date(), Date()) + ) + coEvery { repository.getSkills(true, false) } returns skills + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(3, state.cardState.skills.size) + } + + @Test + fun `Test no data state when fewer than 3 skills`() = runTest { + val skills = listOf( + Skill("1", "Skill 1", "expert", Date(), Date()), + Skill("2", "Skill 2", "advanced", Date(), Date()) + ) + coEvery { repository.getSkills(null, false) } returns skills + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.SUCCESS, state.state) + assertTrue(state.cardState.skills.isEmpty()) + } + + @Test + fun `Test no data state when no skills`() = runTest { + coEvery { repository.getSkills(null, false) } returns emptyList() + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.SUCCESS, state.state) + assertTrue(state.cardState.skills.isEmpty()) + } + + @Test + fun `Test error state when repository throws exception`() = runTest { + coEvery { repository.getSkills(true, false) } throws Exception("Network error") + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.ERROR, state.state) + } + + @Test + fun `Test refresh calls repository with forceNetwork true`() = runTest { + val skills = listOf( + Skill("1", "Skill 1", "advanced", Date(), Date()), + Skill("2", "Skill 2", "proficient", Date(), Date()), + Skill("3", "Skill 3", "beginner", Date(), Date()) + ) + coEvery { repository.getSkills(true, false) } returns skills + coEvery { repository.getSkills(true, true) } returns skills + + val viewModel = getViewModel() + advanceUntilIdle() + + var completed = false + viewModel.uiState.value.onRefresh { completed = true } + advanceUntilIdle() + + assertTrue(completed) + coVerify { repository.getSkills(true, true) } + } + + @Test + fun `Test refresh updates state to loading then success`() = runTest { + val skills = listOf( + Skill("1", "Skill 1", "expert", Date(), Date()), + Skill("2", "Skill 2", "advanced", Date(), Date()), + Skill("3", "Skill 3", "proficient", Date(), Date()) + ) + coEvery { repository.getSkills(null, any()) } returns skills + + val viewModel = getViewModel() + advanceUntilIdle() + + var completed = false + viewModel.uiState.value.onRefresh { completed = true } + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.SUCCESS, state.state) + assertTrue(completed) + } + + @Test + fun `Test refresh with error sets error state`() = runTest { + val skills = listOf( + Skill("1", "Skill 1", "expert", Date(), Date()), + Skill("2", "Skill 2", "advanced", Date(), Date()), + Skill("3", "Skill 3", "proficient", Date(), Date()) + ) + coEvery { repository.getSkills(true, false) } returns skills + coEvery { repository.getSkills(true, true) } throws Exception("Refresh failed") + + val viewModel = getViewModel() + advanceUntilIdle() + + var completed = false + viewModel.uiState.value.onRefresh { completed = true } + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.ERROR, state.state) + assertTrue(completed) + } + + @Test + fun `Test null proficiency level defaults to beginner`() = runTest { + val skills = listOf( + Skill("1", "Skill Without Level", null, Date(), Date()), + Skill("2", "Advanced Skill", "advanced", Date(), Date()), + Skill("3", "Proficient Skill", "proficient", Date(), Date()) + ) + coEvery { repository.getSkills(true, false) } returns skills + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + val skillWithoutLevel = state.cardState.skills.find { it.name == "Skill Without Level" } + assertEquals(SkillHighlightProficiencyLevel.BEGINNER, skillWithoutLevel?.proficiencyLevel) + } + + @Test + fun `Test unknown proficiency level defaults to beginner`() = runTest { + val skills = listOf( + Skill("1", "Unknown Level Skill", "unknown", Date(), Date()), + Skill("2", "Advanced Skill", "advanced", Date(), Date()), + Skill("3", "Proficient Skill", "proficient", Date(), Date()) + ) + coEvery { repository.getSkills(true, false) } returns skills + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + val unknownSkill = state.cardState.skills.find { it.name == "Unknown Level Skill" } + assertEquals(SkillHighlightProficiencyLevel.BEGINNER, unknownSkill?.proficiencyLevel) + } + + @Test + fun `Test proficiency level parsing is case insensitive`() = runTest { + val skills = listOf( + Skill("1", "Skill 1", "EXPERT", Date(), Date()), + Skill("2", "Skill 2", "Advanced", Date(), Date()), + Skill("3", "Skill 3", "ProFicIent", Date(), Date()) + ) + coEvery { repository.getSkills(true, false) } returns skills + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(SkillHighlightProficiencyLevel.EXPERT, state.cardState.skills[0].proficiencyLevel) + assertEquals(SkillHighlightProficiencyLevel.ADVANCED, state.cardState.skills[1].proficiencyLevel) + assertEquals(SkillHighlightProficiencyLevel.PROFICIENT, state.cardState.skills[2].proficiencyLevel) + } + + private fun getViewModel(): DashboardSkillHighlightsViewModel { + return DashboardSkillHighlightsViewModel(repository) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewRepositoryTest.kt new file mode 100644 index 0000000000..3c7b720dd6 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewRepositoryTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skilloverview + +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetSkillsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Skill +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.util.Date + +class DashboardSkillOverviewRepositoryTest { + private val getSkillsManager: GetSkillsManager = mockk(relaxed = true) + + @Test + fun `Test successful skills retrieval with completedOnly true`() = runTest { + val skills = listOf( + Skill("1", "Completed Skill 1", "expert", Date(), Date()), + Skill("2", "Completed Skill 2", "advanced", Date(), Date()), + Skill("3", "Completed Skill 3", "proficient", Date(), Date()) + ) + coEvery { getSkillsManager.getSkills(true, false) } returns skills + + val result = getRepository().getSkills(completedOnly = true, forceNetwork = false) + + assertEquals(3, result.size) + assertEquals(skills, result) + coVerify { getSkillsManager.getSkills(true, false) } + } + + @Test + fun `Test skills retrieval with forceNetwork true`() = runTest { + val skills = listOf( + Skill("1", "Network Skill", "advanced", Date(), Date()) + ) + coEvery { getSkillsManager.getSkills(true, true) } returns skills + + val result = getRepository().getSkills(completedOnly = true, forceNetwork = true) + + assertEquals(1, result.size) + coVerify { getSkillsManager.getSkills(true, true) } + } + + @Test + fun `Test empty skills list is returned correctly`() = runTest { + coEvery { getSkillsManager.getSkills(true, false) } returns emptyList() + + val result = getRepository().getSkills(completedOnly = true, forceNetwork = false) + + assertEquals(0, result.size) + } + + @Test + fun `Test skills with null proficiency level`() = runTest { + val skills = listOf( + Skill("1", "Skill Without Level", null, Date(), Date()) + ) + coEvery { getSkillsManager.getSkills(true, false) } returns skills + + val result = getRepository().getSkills(completedOnly = true, forceNetwork = false) + + assertEquals(1, result.size) + assertEquals(null, result[0].proficiencyLevel) + } + + private fun getRepository(): DashboardSkillOverviewRepository { + return DashboardSkillOverviewRepository(getSkillsManager) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewViewModelTest.kt new file mode 100644 index 0000000000..0f27f44c75 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/skilloverview/DashboardSkillOverviewViewModelTest.kt @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.skilloverview + +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Skill +import com.instructure.horizon.features.dashboard.DashboardItemState +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class DashboardSkillOverviewViewModelTest { + private val repository: DashboardSkillOverviewRepository = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test initialization loads skills data`() = runTest { + val skills = listOf( + Skill("1", "Completed Skill 1", "advanced", Date(), Date()), + Skill("2", "Completed Skill 2", "beginner", Date(), Date()), + Skill("3", "Completed Skill 3", "proficient", Date(), Date()) + ) + coEvery { repository.getSkills(true, false) } returns skills + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.SUCCESS, state.state) + assertEquals(3, state.cardState.completedSkillCount) + coVerify { repository.getSkills(true, false) } + } + + @Test + fun `Test completed skill count is correct`() = runTest { + val skills = listOf( + Skill("1", "Skill 1", "expert", Date(), Date()), + Skill("2", "Skill 2", "advanced", Date(), Date()), + Skill("3", "Skill 3", "proficient", Date(), Date()), + Skill("4", "Skill 4", "beginner", Date(), Date()), + Skill("5", "Skill 5", "advanced", Date(), Date()) + ) + coEvery { repository.getSkills(true, false) } returns skills + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(5, state.cardState.completedSkillCount) + } + + @Test + fun `Test zero completed skills`() = runTest { + coEvery { repository.getSkills(true, false) } returns emptyList() + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.SUCCESS, state.state) + assertEquals(0, state.cardState.completedSkillCount) + } + + @Test + fun `Test error state when repository throws exception`() = runTest { + coEvery { repository.getSkills(true, false) } throws Exception("Network error") + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.ERROR, state.state) + } + + @Test + fun `Test refresh calls repository with forceNetwork true`() = runTest { + val skills = listOf( + Skill("1", "Skill 1", "advanced", Date(), Date()), + Skill("2", "Skill 2", "proficient", Date(), Date()) + ) + coEvery { repository.getSkills(true, false) } returns skills + coEvery { repository.getSkills(true, true) } returns skills + + val viewModel = getViewModel() + advanceUntilIdle() + + var completed = false + viewModel.uiState.value.onRefresh { completed = true } + advanceUntilIdle() + + assertTrue(completed) + coVerify { repository.getSkills(true, true) } + } + + @Test + fun `Test refresh updates state to loading then success`() = runTest { + val skills = listOf( + Skill("1", "Skill 1", "expert", Date(), Date()), + Skill("2", "Skill 2", "advanced", Date(), Date()) + ) + coEvery { repository.getSkills(true, any()) } returns skills + + val viewModel = getViewModel() + advanceUntilIdle() + + var completed = false + viewModel.uiState.value.onRefresh { completed = true } + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.SUCCESS, state.state) + assertTrue(completed) + } + + @Test + fun `Test refresh with error sets error state`() = runTest { + val skills = listOf( + Skill("1", "Skill 1", "expert", Date(), Date()) + ) + coEvery { repository.getSkills(true, false) } returns skills + coEvery { repository.getSkills(true, true) } throws Exception("Refresh failed") + + val viewModel = getViewModel() + advanceUntilIdle() + + var completed = false + viewModel.uiState.value.onRefresh { completed = true } + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.ERROR, state.state) + assertTrue(completed) + } + + @Test + fun `Test large number of completed skills`() = runTest { + val skills = (1..100).map { + Skill(it.toString(), "Skill $it", "advanced", Date(), Date()) + } + coEvery { repository.getSkills(true, false) } returns skills + + val viewModel = getViewModel() + advanceUntilIdle() + + val state = viewModel.uiState.value + assertEquals(100, state.cardState.completedSkillCount) + } + + private fun getViewModel(): DashboardSkillOverviewViewModel { + return DashboardSkillOverviewViewModel(repository) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentRepositoryTest.kt new file mode 100644 index 0000000000..18a890f2a8 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentRepositoryTest.kt @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.timespent + +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetWidgetsManager +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.journey.GetWidgetDataQuery +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +class DashboardTimeSpentRepositoryTest { + + private val getWidgetsManager: GetWidgetsManager = mockk(relaxed = true) + private val getCoursesManager: HorizonGetCoursesManager = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + + private lateinit var repository: DashboardTimeSpentRepository + + @Before + fun setup() { + val user = User(id = 1L) + every { apiPrefs.user } returns user + repository = DashboardTimeSpentRepository(getWidgetsManager, getCoursesManager, apiPrefs) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `getTimeSpentData returns data successfully`() = runTest { + val graphqlData = GetWidgetDataQuery.WidgetData( + lastModifiedDate = Date(), + data = listOf( + mapOf( + "date" to "2025-10-08", + "user_id" to 1.0, + "course_id" to 101.0, + "course_name" to "Test Course", + "minutes_per_day" to 120.0 + ) + ) + ) + + coEvery { getWidgetsManager.getTimeSpentWidgetData(null, false) } returns graphqlData + + val result = repository.getTimeSpentData(null, false) + + assertEquals(graphqlData.lastModifiedDate, result.lastModifiedDate) + assertEquals(1, result.data.size) + assertEquals(101L, result.data[0].courseId) + assertEquals("Test Course", result.data[0].courseName) + assertEquals(120, result.data[0].minutesPerDay) + coVerify { getWidgetsManager.getTimeSpentWidgetData(null, false) } + } + + @Test + fun `getTimeSpentData with courseId passes courseId to manager`() = runTest { + val courseId = 123L + val graphqlData = GetWidgetDataQuery.WidgetData( + lastModifiedDate = Date(), + data = listOf( + mapOf( + "date" to "2025-10-08", + "course_id" to 123.0, + "course_name" to "Filtered Course", + "minutes_per_day" to 90.0 + ) + ) + ) + + coEvery { getWidgetsManager.getTimeSpentWidgetData(courseId, false) } returns graphqlData + + val result = repository.getTimeSpentData(courseId, false) + + assertEquals(graphqlData.lastModifiedDate, result.lastModifiedDate) + assertEquals(1, result.data.size) + assertEquals(123L, result.data[0].courseId) + coVerify { getWidgetsManager.getTimeSpentWidgetData(courseId, false) } + } + + @Test + fun `getTimeSpentData with forceNetwork true uses network`() = runTest { + val graphqlData = GetWidgetDataQuery.WidgetData( + lastModifiedDate = Date(), + data = listOf( + mapOf( + "minutes_per_day" to 150.0 + ) + ) + ) + + coEvery { getWidgetsManager.getTimeSpentWidgetData(null, true) } returns graphqlData + + val result = repository.getTimeSpentData(null, true) + + assertEquals(graphqlData.lastModifiedDate, result.lastModifiedDate) + assertEquals(150, result.data[0].minutesPerDay) + coVerify { getWidgetsManager.getTimeSpentWidgetData(null, true) } + } + + @Test + fun `getCourses returns courses successfully`() = runTest { + val userId = 1L + val courses = listOf( + CourseWithProgress(courseId = 1L, courseName = "Course 1", progress = 0.5), + CourseWithProgress(courseId = 2L, courseName = "Course 2", progress = 0.8) + ) + val dataResult = DataResult.Success(courses) + + coEvery { getCoursesManager.getCoursesWithProgress(userId, false) } returns dataResult + + val result = repository.getCourses(false) + + assertEquals(courses, result) + coVerify { getCoursesManager.getCoursesWithProgress(userId, false) } + } + + @Test + fun `getCourses with forceNetwork true uses network`() = runTest { + val userId = 1L + val courses = listOf( + CourseWithProgress(courseId = 3L, courseName = "Course 3", progress = 0.9) + ) + val dataResult = DataResult.Success(courses) + + coEvery { getCoursesManager.getCoursesWithProgress(userId, true) } returns dataResult + + val result = repository.getCourses(true) + + assertEquals(courses, result) + coVerify { getCoursesManager.getCoursesWithProgress(userId, true) } + } + + @Test + fun `getCourses returns empty list when no courses`() = runTest { + val userId = 1L + val courses = emptyList() + val dataResult = DataResult.Success(courses) + + coEvery { getCoursesManager.getCoursesWithProgress(userId, false) } returns dataResult + + val result = repository.getCourses(false) + + assertEquals(emptyList(), result) + } + + @Test(expected = Exception::class) + fun `getCourses throws exception on failure`() = runTest { + val userId = 1L + + coEvery { getCoursesManager.getCoursesWithProgress(userId, false) } returns DataResult.Fail() + + repository.getCourses(false) + } + + @Test(expected = Exception::class) + fun `getTimeSpentData propagates exceptions`() = runTest { + coEvery { getWidgetsManager.getTimeSpentWidgetData(null, false) } throws Exception("Network error") + + repository.getTimeSpentData(null, false) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentViewModelTest.kt new file mode 100644 index 0000000000..796658de23 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentViewModelTest.kt @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.dashboard.widget.timespent + +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.horizon.features.dashboard.DashboardItemState +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class DashboardTimeSpentViewModelTest { + + private val repository: DashboardTimeSpentRepository = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private lateinit var viewModel: DashboardTimeSpentViewModel + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `init loads time spent data successfully`() = runTest { + val minutesSpend = listOf(300, 450) + val courses = listOf( + CourseWithProgress(courseId = 1L, courseName = "Course 1", progress = 0.0), + CourseWithProgress(courseId = 2L, courseName = "Course 2", progress = 0.0) + ) + val timeSpentData = TimeSpentWidgetData( + lastModifiedDate = Date(), + data = listOf( + TimeSpentDataEntry(courseId = 1L, courseName = "Course 1", minutesPerDay = minutesSpend[0]), + TimeSpentDataEntry(courseId = 2L, courseName = "Course 2", minutesPerDay = minutesSpend[1]) + ) + ) + + coEvery { repository.getCourses(forceNetwork = false) } returns courses + coEvery { repository.getTimeSpentData(courseId = null, forceNetwork = false) } returns timeSpentData + + viewModel = DashboardTimeSpentViewModel(repository) + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.SUCCESS, state.state) + assertEquals((minutesSpend.sum() / 60), state.cardState.hours) + assertEquals((minutesSpend.sum() % 60), state.cardState.minutes) + assertEquals(2, state.cardState.courses.size) + assertEquals("Course 1", state.cardState.courses[0].name) + assertEquals("Course 2", state.cardState.courses[1].name) + } + + @Test + fun `init handles error gracefully`() = runTest { + coEvery { repository.getCourses(forceNetwork = false) } throws Exception("Network error") + + viewModel = DashboardTimeSpentViewModel(repository) + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.ERROR, state.state) + } + + @Test + fun `calculates hours from minutesPerDay correctly`() = runTest { + val minutesSpent = 480 + val courses = listOf( + CourseWithProgress(courseId = 1L, courseName = "Course 1", progress = 0.0) + ) + val timeSpentData = TimeSpentWidgetData( + lastModifiedDate = Date(), + data = listOf( + TimeSpentDataEntry(courseId = 1L, minutesPerDay = minutesSpent) + ) + ) + + coEvery { repository.getCourses(forceNetwork = false) } returns courses + coEvery { repository.getTimeSpentData(courseId = null, forceNetwork = false) } returns timeSpentData + + viewModel = DashboardTimeSpentViewModel(repository) + + val state = viewModel.uiState.value + assertEquals((minutesSpent / 60), state.cardState.hours) + assertEquals((minutesSpent % 60), state.cardState.minutes) + } + + @Test + fun `handles empty data`() = runTest { + val courses = emptyList() + val timeSpentData = TimeSpentWidgetData( + lastModifiedDate = Date(), + data = emptyList() + ) + + coEvery { repository.getCourses(forceNetwork = false) } returns courses + coEvery { repository.getTimeSpentData(courseId = null, forceNetwork = false) } returns timeSpentData + + viewModel = DashboardTimeSpentViewModel(repository) + + val state = viewModel.uiState.value + assertEquals(0, state.cardState.hours) + } + + @Test + fun `filters time spent data by available courses`() = runTest { + val courses = listOf( + CourseWithProgress(courseId = 1L, courseName = "Course 1", progress = 0.0) + ) + val timeSpentData = TimeSpentWidgetData( + lastModifiedDate = Date(), + data = listOf( + TimeSpentDataEntry(courseId = 1L, minutesPerDay = 150), + TimeSpentDataEntry(courseId = 2L, minutesPerDay = 180), + TimeSpentDataEntry(courseId = 999L, minutesPerDay = 300) + ) + ) + + coEvery { repository.getCourses(forceNetwork = false) } returns courses + coEvery { repository.getTimeSpentData(courseId = null, forceNetwork = false) } returns timeSpentData + + viewModel = DashboardTimeSpentViewModel(repository) + + val state = viewModel.uiState.value + assertEquals(150 / 60, state.cardState.hours) + assertEquals(150 % 60, state.cardState.minutes) + } + + @Test + fun `handles null minutesPerDay gracefully`() = runTest { + val courses = listOf( + CourseWithProgress(courseId = 1L, courseName = "Course 1", progress = 0.0) + ) + val timeSpentData = TimeSpentWidgetData( + lastModifiedDate = Date(), + data = listOf( + TimeSpentDataEntry(courseId = 1L, minutesPerDay = null) + ) + ) + + coEvery { repository.getCourses(forceNetwork = false) } returns courses + coEvery { repository.getTimeSpentData(courseId = null, forceNetwork = false) } returns timeSpentData + + viewModel = DashboardTimeSpentViewModel(repository) + + val state = viewModel.uiState.value + assertEquals(0, state.cardState.hours) + } + + @Test + fun `onCourseSelected updates selected course id`() = runTest { + val courses = listOf( + CourseWithProgress(courseId = 1L, courseName = "Math 101", progress = 0.0), + CourseWithProgress(courseId = 2L, courseName = "Science 201", progress = 0.0) + ) + val timeSpentData = TimeSpentWidgetData( + lastModifiedDate = Date(), + data = listOf( + TimeSpentDataEntry(courseId = 1L, minutesPerDay = 120), + TimeSpentDataEntry(courseId = 2L, minutesPerDay = 180) + ) + ) + + coEvery { repository.getCourses(forceNetwork = false) } returns courses + coEvery { repository.getTimeSpentData(courseId = null, forceNetwork = false) } returns timeSpentData + + viewModel = DashboardTimeSpentViewModel(repository) + + val initialState = viewModel.uiState.value + assertEquals(null, initialState.cardState.selectedCourseId) + + initialState.cardState.onCourseSelected("Math 101") + + val updatedState = viewModel.uiState.value + assertEquals(1L, updatedState.cardState.selectedCourseId) + } + + @Test + fun `onCourseSelected with null clears selection`() = runTest { + val courses = listOf( + CourseWithProgress(courseId = 1L, courseName = "Math 101", progress = 0.0) + ) + val timeSpentData = TimeSpentWidgetData( + lastModifiedDate = Date(), + data = listOf( + TimeSpentDataEntry(courseId = 1L, minutesPerDay = 120) + ) + ) + + coEvery { repository.getCourses(forceNetwork = false) } returns courses + coEvery { repository.getTimeSpentData(courseId = null, forceNetwork = false) } returns timeSpentData + + viewModel = DashboardTimeSpentViewModel(repository) + + val initialState = viewModel.uiState.value + initialState.cardState.onCourseSelected("Math 101") + + var updatedState = viewModel.uiState.value + assertEquals(1L, updatedState.cardState.selectedCourseId) + + updatedState.cardState.onCourseSelected(null) + + updatedState = viewModel.uiState.value + assertEquals(null, updatedState.cardState.selectedCourseId) + } + + @Test + fun `refresh calls repository with forceNetwork true`() = runTest { + val courses = emptyList() + val timeSpentData = TimeSpentWidgetData( + lastModifiedDate = Date(), + data = emptyList() + ) + + coEvery { repository.getCourses(forceNetwork = any()) } returns courses + coEvery { repository.getTimeSpentData(courseId = null, forceNetwork = any()) } returns timeSpentData + + viewModel = DashboardTimeSpentViewModel(repository) + + var refreshCompleted = false + viewModel.uiState.value.onRefresh { + refreshCompleted = true + } + + coVerify { repository.getTimeSpentData(forceNetwork = true) } + assertEquals(true, refreshCompleted) + } + + @Test + fun `refresh handles error and completes`() = runTest { + val courses = emptyList() + val timeSpentData = TimeSpentWidgetData( + lastModifiedDate = Date(), + data = emptyList() + ) + + coEvery { repository.getCourses(forceNetwork = false) } returns courses + coEvery { repository.getTimeSpentData(courseId = null, forceNetwork = false) } returns timeSpentData + coEvery { repository.getCourses(forceNetwork = true) } throws Exception("Network error") + + viewModel = DashboardTimeSpentViewModel(repository) + + var refreshCompleted = false + viewModel.uiState.value.onRefresh { + refreshCompleted = true + } + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.ERROR, state.state) + assertEquals(true, refreshCompleted) + } + + @Test + fun `refresh updates state to loading then success`() = runTest { + val courses = emptyList() + val timeSpentData = TimeSpentWidgetData( + lastModifiedDate = Date(), + data = emptyList() + ) + + coEvery { repository.getCourses(forceNetwork = any()) } returns courses + coEvery { repository.getTimeSpentData(courseId = null, forceNetwork = any()) } returns timeSpentData + + viewModel = DashboardTimeSpentViewModel(repository) + + viewModel.uiState.value.onRefresh {} + + val state = viewModel.uiState.value + assertEquals(DashboardItemState.SUCCESS, state.state) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/home/HomeRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/home/HomeRepositoryTest.kt new file mode 100644 index 0000000000..ca9645b2ab --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/home/HomeRepositoryTest.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.home + +import com.instructure.canvasapi2.apis.ThemeAPI +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.models.CanvasTheme +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class HomeRepositoryTest { + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val themeApi: ThemeAPI.ThemeInterface = mockk(relaxed = true) + private val userApi: UserAPI.UsersInterface = mockk(relaxed = true) + private val getCoursesManager: HorizonGetCoursesManager = mockk(relaxed = true) + + private val userId = 1L + + @Before + fun setup() { + every { apiPrefs.user } returns User(id = userId, name = "Test User") + } + + @Test + fun `Test successful theme retrieval`() = runTest { + val theme = CanvasTheme("", "", "", "", "", "", "", "") + coEvery { themeApi.getTheme(any()) } returns DataResult.Success(theme) + + val result = getRepository().getTheme() + + assertEquals(theme, result) + } + + @Test + fun `Test failed theme retrieval returns null`() = runTest { + coEvery { themeApi.getTheme(any()) } returns DataResult.Fail() + + val result = getRepository().getTheme() + + assertNull(result) + } + + @Test + fun `Test successful user retrieval`() = runTest { + val user = User(id = userId, name = "Test User", locale = "en") + coEvery { userApi.getSelf(any()) } returns DataResult.Success(user) + + val result = getRepository().getSelf() + + assertEquals(user, result) + } + + @Test + fun `Test failed user retrieval returns null`() = runTest { + coEvery { userApi.getSelf(any()) } returns DataResult.Fail() + + val result = getRepository().getSelf() + + assertNull(result) + } + + @Test + fun `Test successful courses retrieval`() = runTest { + val courses = listOf( + CourseWithProgress( + courseId = 1L, + courseName = "Course 1", + courseSyllabus = "", + progress = 50.0 + ), + CourseWithProgress( + courseId = 2L, + courseName = "Course 2", + courseSyllabus = "", + progress = 75.0 + ) + ) + coEvery { getCoursesManager.getCoursesWithProgress(userId, false) } returns DataResult.Success(courses) + + val result = getRepository().getCourses() + + assertEquals(2, result.size) + assertEquals(courses, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed courses retrieval throws exception`() = runTest { + coEvery { getCoursesManager.getCoursesWithProgress(userId, false) } returns DataResult.Fail() + + getRepository().getCourses() + } + + private fun getRepository(): HomeRepository { + return HomeRepository(apiPrefs, themeApi, userApi, getCoursesManager) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/home/HomeViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/home/HomeViewModelTest.kt new file mode 100644 index 0000000000..9d5d50e61e --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/home/HomeViewModelTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.home + +import android.content.Context +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.canvasapi2.models.CanvasTheme +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.horizon.features.aiassistant.common.AiAssistContextProvider +import com.instructure.pandautils.utils.LocaleUtils +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class HomeViewModelTest { + private val context: Context = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val homeRepository: HomeRepository = mockk(relaxed = true) + private val localeUtils: LocaleUtils = mockk(relaxed = true) + private val aiAssistContextProvider: AiAssistContextProvider = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val testUser = User(id = 1L, name = "Test User", locale = "en") + private val testTheme = CanvasTheme("", "", "", "", "", "", "", "") + private val testCourses = listOf( + CourseWithProgress( + courseId = 1L, + courseName = "Course 1", + courseSyllabus = "", + progress = 50.0 + ), + CourseWithProgress( + courseId = 2L, + courseName = "Course 2", + courseSyllabus = "", + progress = 75.0 + ) + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + every { apiPrefs.effectiveLocale } returns "en" + coEvery { homeRepository.getSelf() } returns testUser + coEvery { homeRepository.getTheme() } returns testTheme + coEvery { homeRepository.getCourses() } returns testCourses + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test successful data load updates UI state`() = runTest { + val viewModel = getViewModel() + + val state = viewModel.uiState.value + + assertFalse(state.initialDataLoading) + assertEquals(testTheme, state.theme) + verify { apiPrefs.user = testUser } + } + + @Test + fun `Test theme is loaded from repository`() = runTest { + val viewModel = getViewModel() + + coVerify { homeRepository.getTheme() } + assertEquals(testTheme, viewModel.uiState.value.theme) + } + + @Test + fun `Test user is saved to API prefs`() = runTest { + val viewModel = getViewModel() + + verify { apiPrefs.user = testUser } + } + + @Test + fun `Test courses are loaded`() = runTest { + val viewModel = getViewModel() + + coVerify { homeRepository.getCourses() } + } + + @Test + fun `Test locale change triggers app restart`() = runTest { + every { apiPrefs.effectiveLocale } returnsMany listOf("en", "es") + + val viewModel = getViewModel() + + verify { localeUtils.restartApp(context) } + } + + @Test + fun `Test no restart when locale unchanged`() = runTest { + every { apiPrefs.effectiveLocale } returns "en" + + val viewModel = getViewModel() + + verify(exactly = 0) { localeUtils.restartApp(any()) } + } + + @Test + fun `Test failed data load sets loading to false`() = runTest { + coEvery { homeRepository.getSelf() } throws Exception("Network error") + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.initialDataLoading) + } + + @Test + fun `Test updateShowAiAssist updates state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateShowAiAssist(true) + + assertTrue(viewModel.uiState.value.showAiAssist) + } + + @Test + fun `Test updateShowAiAssist sets AI context with course IDs`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateShowAiAssist(true) + + verify { aiAssistContextProvider.aiAssistContext = match { it.contextSources.size == 2 } } + } + + @Test + fun `Test null user does not crash`() = runTest { + coEvery { homeRepository.getSelf() } returns null + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.initialDataLoading) + verify(exactly = 0) { apiPrefs.user = any() } + } + + @Test + fun `Test null theme does not crash`() = runTest { + coEvery { homeRepository.getTheme() } returns null + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.initialDataLoading) + assertNotNull(viewModel.uiState.value) + } + + private fun getViewModel(): HomeViewModel { + return HomeViewModel(context, apiPrefs, homeRepository, localeUtils, aiAssistContextProvider) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/attachment/HorizonInboxAttachmentPickerViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/attachment/HorizonInboxAttachmentPickerViewModelTest.kt new file mode 100644 index 0000000000..8257c7a0ce --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/attachment/HorizonInboxAttachmentPickerViewModelTest.kt @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.inbox.attachment + +import android.net.Uri +import com.instructure.canvasapi2.managers.FileUploadManager +import com.instructure.canvasapi2.models.Attachment +import com.instructure.canvasapi2.models.postmodels.FileSubmitObject +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.features.file.upload.FileUploadUtilsHelper +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class HorizonInboxAttachmentPickerViewModelTest { + private val fileUploadManager: FileUploadManager = mockk(relaxed = true) + private val fileUploadUtils: FileUploadUtilsHelper = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val testUri: Uri = mockk(relaxed = true) + private val testFileSubmitObject = FileSubmitObject( + name = "test.pdf", + size = 1000L, + contentType = "application/pdf", + fullPath = "/path/to/test.pdf" + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + mockkStatic(Dispatchers::class) + every { Dispatchers.IO } returns testDispatcher + coEvery { fileUploadUtils.getFileSubmitObjectFromInputStream(any(), any(), any()) } returns testFileSubmitObject + every { fileUploadUtils.getFileNameWithDefault(any()) } returns "test.pdf" + every { fileUploadUtils.getFileMimeType(any()) } returns "application/pdf" + coEvery { fileUploadManager.uploadFile(any(), any(), any()) } returns DataResult.Success( + Attachment(id = 123L) + ) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel initializes with empty file list`() = runTest { + val viewModel = getViewModel() + + assertTrue(viewModel.uiState.value.files.isEmpty()) + assertNotNull(viewModel.uiState.value.onFileSelected) + } + + @Test + fun `Test file selection adds file to list`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + assertEquals(1, viewModel.uiState.value.files.size) + assertEquals("test.pdf", viewModel.uiState.value.files.first().fileName) + assertEquals(1000L, viewModel.uiState.value.files.first().fileSize) + } + + @Test + fun `Test file upload starts with in-progress state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + val file = viewModel.uiState.value.files.first() + // Initially in progress, but will complete quickly due to UnconfinedTestDispatcher + assertTrue(file.state is HorizonInboxAttachmentState.InProgress || file.state is HorizonInboxAttachmentState.Success) + } + + @Test + fun `Test successful file upload sets success state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + // Wait for upload to complete + val file = viewModel.uiState.value.files.first() + assertTrue(file.state is HorizonInboxAttachmentState.Success) + assertEquals(123L, file.id) + } + + @Test + fun `Test failed file upload sets error state`() = runTest { + coEvery { fileUploadManager.uploadFile(any(), any(), any()) } returns DataResult.Fail() + + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + val file = viewModel.uiState.value.files.first() + assertTrue(file.state is HorizonInboxAttachmentState.Error) + } + + @Test + fun `Test successful upload adds remove action`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + val file = viewModel.uiState.value.files.first() + assertNotNull(file.onActionClicked) + } + + @Test + fun `Test remove action removes file from list`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + val file = viewModel.uiState.value.files.first() + file.onActionClicked?.invoke() + + assertTrue(viewModel.uiState.value.files.isEmpty()) + } + + @Test + fun `Test multiple file uploads`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + viewModel.uiState.value.onFileSelected(testUri) + + assertEquals(2, viewModel.uiState.value.files.size) + } + + @Test + fun `Test upload progress updates file state`() = runTest { + // This test verifies the upload manager's progress listener is called + coEvery { fileUploadManager.uploadFile(any(), any(), any()) } coAnswers { + val listener = secondArg() + listener.onProgressUpdated(50f, 1000L) + DataResult.Success(Attachment(id = 123L)) + } + + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + // File should eventually reach success state + val file = viewModel.uiState.value.files.first() + assertTrue(file.state is HorizonInboxAttachmentState.Success) + } + + @Test + fun `Test failed upload adds retry action`() = runTest { + coEvery { fileUploadManager.uploadFile(any(), any(), any()) } returns DataResult.Fail() + + val viewModel = getViewModel() + + viewModel.uiState.value.onFileSelected(testUri) + + val file = viewModel.uiState.value.files.first() + assertTrue(file.state is HorizonInboxAttachmentState.Error) + assertNotNull(file.onActionClicked) + } + + private fun getViewModel(): HorizonInboxAttachmentPickerViewModel { + return HorizonInboxAttachmentPickerViewModel(fileUploadManager, fileUploadUtils) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeRepositoryTest.kt new file mode 100644 index 0000000000..33c1ff5fb8 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeRepositoryTest.kt @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.inbox.compose + +import com.instructure.canvasapi2.CanvasRestAdapter +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.InboxApi +import com.instructure.canvasapi2.apis.RecipientAPI +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Recipient +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkAll +import io.mockk.verify +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +class HorizonInboxComposeRepositoryTest { + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val recipientApi: RecipientAPI.RecipientInterface = mockk(relaxed = true) + private val inboxApi: InboxApi.InboxInterface = mockk(relaxed = true) + + private lateinit var repository: HorizonInboxComposeRepository + + private val testCourses = listOf( + Course(id = 1L, name = "Course 1"), + Course(id = 2L, name = "Course 2") + ) + + private val testRecipients = listOf( + Recipient(stringId = "1", name = "Student 1"), + Recipient(stringId = "2", name = "Student 2"), + ) + + @Before + fun setup() { + repository = HorizonInboxComposeRepository(courseApi, recipientApi, inboxApi) + mockkObject(CanvasRestAdapter) + every { CanvasRestAdapter.clearCacheUrls(any()) } returns Unit + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `getAllInboxCourses returns course list`() = runTest { + coEvery { courseApi.getFirstPageCoursesInbox(any()) } returns DataResult.Success(testCourses) + + val result = repository.getAllInboxCourses(forceNetwork = true) + + assertEquals(2, result.size) + assertEquals("Course 1", result.first().name) + coVerify { courseApi.getFirstPageCoursesInbox(any()) } + } + + @Test + fun `getAllInboxCourses with forceNetwork false`() = runTest { + coEvery { courseApi.getFirstPageCoursesInbox(any()) } returns DataResult.Success(testCourses) + + repository.getAllInboxCourses(forceNetwork = false) + + coVerify { courseApi.getFirstPageCoursesInbox(match { !it.isForceReadFromNetwork }) } + } + + @Test + fun `getAllInboxCourses with forceNetwork true`() = runTest { + coEvery { courseApi.getFirstPageCoursesInbox(any()) } returns DataResult.Success(testCourses) + + repository.getAllInboxCourses(forceNetwork = true) + + coVerify { courseApi.getFirstPageCoursesInbox(match { it.isForceReadFromNetwork }) } + } + + @Test + fun `getRecipients returns filtered list`() = runTest { + coEvery { recipientApi.getFirstPageRecipientList(any(), any(), any()) } returns DataResult.Success(testRecipients) + + val result = repository.getRecipients(courseId = 1L, searchQuery = "student") + + assertEquals(2, result.size) + assertTrue(result.all { it.recipientType == Recipient.Type.Person }) + coVerify { recipientApi.getFirstPageRecipientList("student", "1", any()) } + } + + @Test + fun `getRecipients filters out non-person recipients`() = runTest { + coEvery { recipientApi.getFirstPageRecipientList(any(), any(), any()) } returns DataResult.Success(testRecipients) + + val result = repository.getRecipients(courseId = 1L, searchQuery = null) + + assertEquals(2, result.size) + assertTrue(result.none { it.recipientType == Recipient.Type.Group }) + } + + @Test + fun `getRecipients with null search query`() = runTest { + coEvery { recipientApi.getFirstPageRecipientList(any(), any(), any()) } returns DataResult.Success(testRecipients) + + repository.getRecipients(courseId = 1L, searchQuery = null) + + coVerify { recipientApi.getFirstPageRecipientList(null, "1", any()) } + } + + @Test + fun `createConversation calls API with correct parameters`() = runTest { + coEvery { inboxApi.createConversation(any(), any(), any(), any(), any(), any(), any()) } returns mockk(relaxed = true) + + repository.createConversation( + recipientIds = listOf("1", "2"), + body = "Test message", + subject = "Test subject", + contextCode = "course_1", + attachmentIds = longArrayOf(100L, 200L), + isBulkMessage = false + ) + + coVerify { + inboxApi.createConversation( + recipients = listOf("1", "2"), + message = "Test message", + subject = "Test subject", + contextCode = "course_1", + isBulk = 0, + attachmentIds = longArrayOf(100L, 200L), + params = any() + ) + } + } + + @Test + fun `createConversation with bulk message flag`() = runTest { + coEvery { inboxApi.createConversation(any(), any(), any(), any(), any(), any(), any()) } returns mockk(relaxed = true) + + repository.createConversation( + recipientIds = listOf("1", "2"), + body = "Bulk message", + subject = "Bulk subject", + contextCode = "course_1", + attachmentIds = longArrayOf(), + isBulkMessage = true + ) + + coVerify { + inboxApi.createConversation( + recipients = any(), + message = any(), + subject = any(), + contextCode = any(), + isBulk = 1, + attachmentIds = any(), + params = any() + ) + } + } + + @Test + fun `invalidateConversationListCachedResponse clears cache`() { + repository.invalidateConversationListCachedResponse() + + verify { CanvasRestAdapter.clearCacheUrls("conversations") } + } + + @Test + fun `createConversation with empty attachments`() = runTest { + coEvery { inboxApi.createConversation(any(), any(), any(), any(), any(), any(), any()) } returns mockk(relaxed = true) + + repository.createConversation( + recipientIds = listOf("1"), + body = "Message", + subject = "Subject", + contextCode = "course_1", + attachmentIds = longArrayOf(), + isBulkMessage = false + ) + + coVerify { + inboxApi.createConversation( + recipients = any(), + message = any(), + subject = any(), + contextCode = any(), + isBulk = any(), + attachmentIds = longArrayOf(), + params = any() + ) + } + } + + @Test + fun `getRecipients with forceNetwork parameter`() = runTest { + coEvery { recipientApi.getFirstPageRecipientList(any(), any(), any()) } returns DataResult.Success(testRecipients) + + repository.getRecipients(courseId = 1L, searchQuery = "test", forceNetwork = true) + + coVerify { recipientApi.getFirstPageRecipientList(any(), any(), match { it.isForceReadFromNetwork }) } + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeViewModelTest.kt new file mode 100644 index 0000000000..8ea93b9ecb --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeViewModelTest.kt @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.inbox.compose + +import android.content.Context +import androidx.compose.ui.text.input.TextFieldValue +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Recipient +import com.instructure.horizon.features.inbox.InboxEventHandler +import com.instructure.horizon.features.inbox.attachment.HorizonInboxAttachment +import com.instructure.horizon.features.inbox.attachment.HorizonInboxAttachmentState +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class HorizonInboxComposeViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: HorizonInboxComposeRepository = mockk(relaxed = true) + private val inboxEventHandler: InboxEventHandler = InboxEventHandler() + private val testDispatcher = UnconfinedTestDispatcher() + + private val testCourses = listOf( + Course(id = 1L, name = "Course 1"), + Course(id = 2L, name = "Course 2") + ) + + private val testRecipients = listOf( + Recipient(stringId = "1", name = "Recipient 1"), + Recipient(stringId = "2", name = "Recipient 2") + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + coEvery { repository.getAllInboxCourses(any()) } returns testCourses + coEvery { repository.getRecipients(any(), any()) } returns testRecipients + coEvery { repository.createConversation(any(), any(), any(), any(), any(), any()) } returns Unit + coEvery { repository.invalidateConversationListCachedResponse() } returns Unit + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel initializes with course list`() = runTest { + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.coursePickerOptions.isEmpty()) + assertEquals(2, viewModel.uiState.value.coursePickerOptions.size) + coVerify { repository.getAllInboxCourses(forceNetwork = true) } + } + + @Test + fun `Test single course is auto-selected`() = runTest { + coEvery { repository.getAllInboxCourses(any()) } returns listOf(testCourses.first()) + + val viewModel = getViewModel() + + assertNotNull(viewModel.uiState.value.selectedCourse) + assertEquals(1L, viewModel.uiState.value.selectedCourse?.id) + } + + @Test + fun `Test multiple courses not auto-selected`() = runTest { + val viewModel = getViewModel() + + assertNull(viewModel.uiState.value.selectedCourse) + } + + @Test + fun `Test course selection updates state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onCourseSelected(testCourses.first()) + + assertEquals(1L, viewModel.uiState.value.selectedCourse?.id) + assertNull(viewModel.uiState.value.courseErrorMessage) + } + + @Test + fun `Test recipient search query change triggers fetch`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.onCourseSelected(testCourses.first()) + + viewModel.uiState.value.onRecipientSearchQueryChanged(TextFieldValue("test")) + + assertEquals("test", viewModel.uiState.value.recipientSearchQuery.text) + } + + @Test + fun `Test recipient selection adds to list`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onRecipientSelected(testRecipients.first()) + + assertTrue(viewModel.uiState.value.selectedRecipients.contains(testRecipients.first())) + assertEquals("", viewModel.uiState.value.recipientSearchQuery.text) + assertNull(viewModel.uiState.value.recipientErrorMessage) + } + + @Test + fun `Test recipient removal updates list`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.onRecipientSelected(testRecipients.first()) + + viewModel.onRecipientRemoved(testRecipients.first()) + + assertFalse(viewModel.uiState.value.selectedRecipients.contains(testRecipients.first())) + } + + @Test + fun `Test send individually checkbox updates state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onSendIndividuallyChanged(true) + + assertTrue(viewModel.uiState.value.isSendIndividually) + } + + @Test + fun `Test subject change updates state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onSubjectChanged(TextFieldValue("Test Subject")) + + assertEquals("Test Subject", viewModel.uiState.value.subject.text) + assertNull(viewModel.uiState.value.subjectErrorMessage) + } + + @Test + fun `Test body change updates state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onBodyChanged(TextFieldValue("Test Body")) + + assertEquals("Test Body", viewModel.uiState.value.body.text) + assertNull(viewModel.uiState.value.bodyErrorMessage) + } + + @Test + fun `Test send conversation validates required fields`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onSendConversation({}) + + assertNotNull(viewModel.uiState.value.courseErrorMessage) + assertNotNull(viewModel.uiState.value.recipientErrorMessage) + assertNotNull(viewModel.uiState.value.subjectErrorMessage) + assertNotNull(viewModel.uiState.value.bodyErrorMessage) + } + + @Test + fun `Test send conversation with valid data succeeds`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.onCourseSelected(testCourses.first()) + viewModel.uiState.value.onRecipientSelected(testRecipients.first()) + viewModel.uiState.value.onSubjectChanged(TextFieldValue("Subject")) + viewModel.uiState.value.onBodyChanged(TextFieldValue("Body")) + + var finished = false + viewModel.uiState.value.onSendConversation { finished = true } + + coVerify { repository.createConversation(any(), any(), any(), any(), any(), any()) } + assertTrue(finished) + } + + @Test + fun `Test send conversation validates attachments are uploaded`() = runTest { + val viewModel = getViewModel() + val failedAttachment = HorizonInboxAttachment( + id = 1L, + fileName = "test.pdf", + fileSize = 1000L, + filePath = "/path", + state = HorizonInboxAttachmentState.Error + ) + viewModel.uiState.value.onCourseSelected(testCourses.first()) + viewModel.uiState.value.onRecipientSelected(testRecipients.first()) + viewModel.uiState.value.onSubjectChanged(TextFieldValue("Subject")) + viewModel.uiState.value.onBodyChanged(TextFieldValue("Body")) + viewModel.uiState.value.onAttachmentsChanged(listOf(failedAttachment)) + + viewModel.uiState.value.onSendConversation({}) + + assertNotNull(viewModel.uiState.value.attachmentsErrorMessage) + } + + @Test + fun `Test attachment picker visibility toggle`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onShowAttachmentPickerChanged(true) + + assertTrue(viewModel.uiState.value.showAttachmentPicker) + } + + @Test + fun `Test attachments change updates state`() = runTest { + val viewModel = getViewModel() + val attachment = HorizonInboxAttachment( + id = 1L, + fileName = "test.pdf", + fileSize = 1000L, + filePath = "/path", + state = HorizonInboxAttachmentState.Success + ) + + viewModel.uiState.value.onAttachmentsChanged(listOf(attachment)) + + assertEquals(1, viewModel.uiState.value.attachments.size) + assertEquals("test.pdf", viewModel.uiState.value.attachments.first().fileName) + } + + @Test + fun `Test exit confirmation dialog visibility toggle`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateShowExitConfirmationDialog(true) + + assertTrue(viewModel.uiState.value.showExitConfirmationDialog) + } + + @Test + fun `Test snackbar dismiss clears message`() = runTest { + coEvery { repository.getAllInboxCourses(any()) } throws Exception("Error") + + val viewModel = getViewModel() + assertNotNull(viewModel.uiState.value.snackbarMessage) + + viewModel.uiState.value.onDismissSnackbar() + + assertNull(viewModel.uiState.value.snackbarMessage) + } + + @Test + fun `Test fetch recipients with valid course`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.onCourseSelected(testCourses.first()) + viewModel.uiState.value.onRecipientSearchQueryChanged(TextFieldValue("test query")) + + // Wait for debounce + delay(250) + + coVerify { repository.getRecipients(courseId = 1L, searchQuery = "test query", any()) } + } + + private fun getViewModel(): HorizonInboxComposeViewModel { + return HorizonInboxComposeViewModel(repository, context, inboxEventHandler) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsRepositoryTest.kt new file mode 100644 index 0000000000..32293fbb79 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsRepositoryTest.kt @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.inbox.details + +import com.instructure.canvasapi2.CanvasRestAdapter +import com.instructure.canvasapi2.apis.AccountNotificationAPI +import com.instructure.canvasapi2.apis.AnnouncementAPI +import com.instructure.canvasapi2.apis.DiscussionAPI +import com.instructure.canvasapi2.apis.InboxApi +import com.instructure.canvasapi2.models.AccountNotification +import com.instructure.canvasapi2.models.BasicUser +import com.instructure.canvasapi2.models.Conversation +import com.instructure.canvasapi2.models.DiscussionEntry +import com.instructure.canvasapi2.models.DiscussionParticipant +import com.instructure.canvasapi2.models.DiscussionTopic +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkAll +import io.mockk.verify +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +class HorizonInboxDetailsRepositoryTest { + private val inboxApi: InboxApi.InboxInterface = mockk(relaxed = true) + private val accountNotificationApi: AccountNotificationAPI.AccountNotificationInterface = mockk(relaxed = true) + private val announcementApi: AnnouncementAPI.AnnouncementInterface = mockk(relaxed = true) + private val discussionApi: DiscussionAPI.DiscussionInterface = mockk(relaxed = true) + + private lateinit var repository: HorizonInboxDetailsRepository + + private val testConversation = Conversation( + id = 1L, + subject = "Test Conversation", + participants = mutableListOf(BasicUser(id = 1L, name = "User 1")) + ) + + private val testAccountNotification = AccountNotification( + id = 100L, + subject = "Account Announcement", + message = "Test message" + ) + + private val testAnnouncement = DiscussionTopicHeader( + id = 200L, + title = "Course Announcement", + message = "Announcement message", + author = DiscussionParticipant(id = 1L, displayName = "Teacher") + ) + + private val testDiscussionTopic = DiscussionTopic( + views = mutableListOf( + DiscussionEntry(id = 1L, message = "Reply 1"), + DiscussionEntry(id = 2L, message = "Reply 2") + ) + ) + + @Before + fun setup() { + repository = HorizonInboxDetailsRepository(inboxApi, accountNotificationApi, announcementApi, discussionApi) + mockkObject(CanvasRestAdapter) + every { CanvasRestAdapter.clearCacheUrls(any()) } returns Unit + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `getConversation returns conversation`() = runTest { + coEvery { inboxApi.getConversation(any(), any(), any()) } returns DataResult.Success(testConversation) + + val result = repository.getConversation(id = 1L, forceRefresh = false) + + assertEquals("Test Conversation", result.subject) + coVerify { inboxApi.getConversation(1L, true, any()) } + } + + @Test + fun `getConversation with forceRefresh true`() = runTest { + coEvery { inboxApi.getConversation(any(), any(), any()) } returns DataResult.Success(testConversation) + + repository.getConversation(id = 1L, forceRefresh = true) + + coVerify { inboxApi.getConversation(any(), any(), match { it.isForceReadFromNetwork }) } + } + + @Test + fun `getConversation with markAsRead false`() = runTest { + coEvery { inboxApi.getConversation(any(), any(), any()) } returns DataResult.Success(testConversation) + + repository.getConversation(id = 1L, markAsRead = false, forceRefresh = false) + + coVerify { inboxApi.getConversation(1L, false, any()) } + } + + @Test + fun `getAccountAnnouncement returns filtered announcement`() = runTest { + val announcements = listOf( + testAccountNotification, + AccountNotification(id = 101L, subject = "Other", message = "Other message") + ) + coEvery { accountNotificationApi.getAccountNotifications(any(), any(), any()) } returns DataResult.Success(announcements) + + val result = repository.getAccountAnnouncement(id = 100L, forceRefresh = false) + + assertEquals("Account Announcement", result.subject) + coVerify { accountNotificationApi.getAccountNotifications(any(), true, true) } + } + + @Test + fun `getAnnouncement returns course announcement`() = runTest { + coEvery { announcementApi.getCourseAnnouncement(any(), any(), any()) } returns DataResult.Success(testAnnouncement) + + val result = repository.getAnnouncement(id = 200L, courseId = 1L, forceRefresh = false) + + assertEquals("Course Announcement", result.title) + coVerify { announcementApi.getCourseAnnouncement(1L, 200L, any()) } + } + + @Test + fun `getAnnouncementTopic returns discussion topic`() = runTest { + coEvery { discussionApi.getFullDiscussionTopic(any(), any(), any(), any(), any()) } returns DataResult.Success(testDiscussionTopic) + + val result = repository.getAnnouncementTopic(id = 200L, courseId = 1L, forceRefresh = false) + + assertEquals(2, result.views.size) + coVerify { discussionApi.getFullDiscussionTopic("courses", 1L, 200L, 1, any()) } + } + + @Test + fun `markAnnouncementAsRead marks topic and entries as read`() = runTest { + coEvery { discussionApi.markDiscussionTopicRead(any(), any(), any(), any()) } returns DataResult.Success(Unit) + coEvery { discussionApi.markDiscussionTopicEntryRead(any(), any(), any(), any(), any()) } returns DataResult.Success(Unit) + + val result = repository.markAnnouncementAsRead( + courseId = 1L, + announcementId = 200L, + entries = setOf(1L, 2L) + ) + + assertTrue(result is DataResult.Success) + coVerify { discussionApi.markDiscussionTopicRead("courses", 1L, 200L, any()) } + coVerify(exactly = 2) { discussionApi.markDiscussionTopicEntryRead("courses", 1L, 200L, any(), any()) } + } + + @Test + fun `markAnnouncementAsRead fails if topic marking fails`() = runTest { + coEvery { discussionApi.markDiscussionTopicRead(any(), any(), any(), any()) } returns DataResult.Fail() + + val result = repository.markAnnouncementAsRead( + courseId = 1L, + announcementId = 200L, + entries = setOf(1L, 2L) + ) + + assertTrue(result is DataResult.Fail) + } + + @Test + fun `markAnnouncementAsRead fails if not all entries marked`() = runTest { + coEvery { discussionApi.markDiscussionTopicRead(any(), any(), any(), any()) } returns DataResult.Success(Unit) + coEvery { discussionApi.markDiscussionTopicEntryRead(any(), any(), any(), 1L, any()) } returns DataResult.Success(Unit) + coEvery { discussionApi.markDiscussionTopicEntryRead(any(), any(), any(), 2L, any()) } returns DataResult.Fail() + + val result = repository.markAnnouncementAsRead( + courseId = 1L, + announcementId = 200L, + entries = setOf(1L, 2L) + ) + + assertTrue(result is DataResult.Fail) + } + + @Test + fun `addMessageToConversation adds message successfully`() = runTest { + coEvery { inboxApi.addMessage(any(), any(), any(), any(), any(), any(), any()) } returns DataResult.Success(testConversation) + + val result = repository.addMessageToConversation( + contextCode = "course_1", + conversationId = 1L, + recipientIds = listOf("1", "2"), + body = "Reply message", + includedMessageIds = listOf(10L, 20L), + attachmentIds = listOf(100L, 200L) + ) + + assertEquals("Test Conversation", result.subject) + coVerify { + inboxApi.addMessage( + conversationId = 1L, + recipientIds = listOf("1", "2"), + body = "Reply message", + includedMessageIds = longArrayOf(10L, 20L), + attachmentIds = longArrayOf(100L, 200L), + contextCode = "course_1", + params = any() + ) + } + } + + @Test + fun `addMessageToConversation with empty attachments`() = runTest { + coEvery { inboxApi.addMessage(any(), any(), any(), any(), any(), any(), any()) } returns DataResult.Success(testConversation) + + repository.addMessageToConversation( + contextCode = "course_1", + conversationId = 1L, + recipientIds = listOf("1"), + body = "Reply", + includedMessageIds = listOf(), + attachmentIds = listOf() + ) + + coVerify { + inboxApi.addMessage( + conversationId = any(), + recipientIds = any(), + body = any(), + includedMessageIds = longArrayOf(), + attachmentIds = longArrayOf(), + contextCode = any(), + params = any() + ) + } + } + + @Test + fun `invalidateConversationDetailsCachedResponse clears cache`() { + repository.invalidateConversationDetailsCachedResponse(conversationId = 1L) + + verify { CanvasRestAdapter.clearCacheUrls("conversations/1") } + } + + @Test + fun `markAnnouncementAsRead with empty entries succeeds`() = runTest { + coEvery { discussionApi.markDiscussionTopicRead(any(), any(), any(), any()) } returns DataResult.Success(Unit) + + val result = repository.markAnnouncementAsRead( + courseId = 1L, + announcementId = 200L, + entries = emptySet() + ) + + assertTrue(result is DataResult.Success) + coVerify(exactly = 0) { discussionApi.markDiscussionTopicEntryRead(any(), any(), any(), any(), any()) } + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsViewModelTest.kt new file mode 100644 index 0000000000..e28703809b --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsViewModelTest.kt @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.inbox.details + +import android.content.Context +import android.webkit.URLUtil +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.SavedStateHandle +import androidx.work.WorkManager +import com.instructure.canvasapi2.models.Attachment +import com.instructure.canvasapi2.models.BasicUser +import com.instructure.canvasapi2.models.Conversation +import com.instructure.canvasapi2.models.DiscussionParticipant +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Message +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.horizon.features.dashboard.DashboardEventHandler +import com.instructure.horizon.features.inbox.HorizonInboxItemType +import com.instructure.horizon.features.inbox.InboxEventHandler +import com.instructure.pandautils.room.appdatabase.daos.FileDownloadProgressDao +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class HorizonInboxDetailsViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: HorizonInboxDetailsRepository = mockk(relaxed = true) + private val workManager: WorkManager = mockk(relaxed = true) + private val fileDownloadProgressDao: FileDownloadProgressDao = mockk(relaxed = true) + private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) + private val inboxEventHandler: InboxEventHandler = InboxEventHandler() + private val dashboardEventHandler: DashboardEventHandler = DashboardEventHandler() + private val testDispatcher = UnconfinedTestDispatcher() + + private val testConversation = Conversation( + id = 1L, + subject = "Test Conversation", + messages = listOf( + Message( + id = 1L, + authorId = 1L, + body = "Test message", + createdAt = "2025-01-01T00:00:00Z", + attachments = arrayListOf() + ) + ), + participants = mutableListOf( + BasicUser(id = 1L, name = "Test User") + ) + ) + + private val testAnnouncement = DiscussionTopicHeader( + id = 1L, + title = "Test Announcement", + message = "Test message", + postedDate = Date(), + author = DiscussionParticipant(id = 1L, displayName = "Test Author") + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + mockkStatic(URLUtil::class) + every { URLUtil.isNetworkUrl(any()) } returns true + every { savedStateHandle.get(any()) } returns 1L + every { savedStateHandle.get("courseId") } returns "1" + every { savedStateHandle.get("type") } returns HorizonInboxItemType.Inbox.navigationValue + coEvery { repository.getConversation(any(), any(), any()) } returns testConversation + coEvery { repository.getAnnouncement(any(), any(), any()) } returns testAnnouncement + coEvery { repository.getAnnouncementTopic(any(), any(), any()) } returns mockk(relaxed = true) { + every { views } returns mutableListOf() + } + coEvery { repository.markAnnouncementAsRead(any(), any(), any()) } returns DataResult.Success(Unit) + coEvery { repository.addMessageToConversation(any(), any(), any(), any(), any(), any()) } returns testConversation + coEvery { repository.invalidateConversationDetailsCachedResponse(any()) } returns Unit + coEvery { fileDownloadProgressDao.findByWorkerIdFlow(any()) } returns flowOf(null) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel loads inbox conversation`() = runTest { + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + assertEquals("Test Conversation", viewModel.uiState.value.title) + assertEquals(1, viewModel.uiState.value.items.size) + assertNotNull(viewModel.uiState.value.replyState) + coVerify { repository.getConversation(1L, true, any()) } + } + + @Test + fun `Test conversation message details are mapped correctly`() = runTest { + val viewModel = getViewModel() + + val item = viewModel.uiState.value.items.first() + assertEquals("Test User", item.author) + assertEquals("Test message", item.content) + assertFalse(item.isHtmlContent) + } + + @Test + fun `Test reply text change updates state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.replyState?.onReplyTextValueChange?.invoke(TextFieldValue("Reply text")) + + assertEquals("Reply text", viewModel.uiState.value.replyState?.replyTextValue?.text) + } + + @Test + fun `Test send reply adds message to conversation`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.replyState?.onReplyTextValueChange?.invoke(TextFieldValue("Reply text")) + + viewModel.uiState.value.replyState?.onSendReply?.invoke() + + coVerify { repository.addMessageToConversation(any(), any(), any(), "Reply text", any(), any()) } + coVerify { repository.invalidateConversationDetailsCachedResponse(1L) } + } + + @Test + fun `Test send reply clears input after success`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.replyState?.onReplyTextValueChange?.invoke(TextFieldValue("Reply text")) + + viewModel.uiState.value.replyState?.onSendReply?.invoke() + + assertEquals("", viewModel.uiState.value.replyState?.replyTextValue?.text) + assertFalse(viewModel.uiState.value.replyState?.isLoading == true) + } + + @Test + fun `Test send reply handles error`() = runTest { + coEvery { repository.addMessageToConversation(any(), any(), any(), any(), any(), any()) } throws Exception("Error") + + val viewModel = getViewModel() + viewModel.uiState.value.replyState?.onReplyTextValueChange?.invoke(TextFieldValue("Reply text")) + + viewModel.uiState.value.replyState?.onSendReply?.invoke() + + assertNotNull(viewModel.uiState.value.loadingState.snackbarMessage) + assertFalse(viewModel.uiState.value.replyState?.isLoading == true) + } + + @Test + fun `Test refresh reloads data`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.loadingState.onRefresh() + + coVerify(exactly = 2) { repository.getConversation(1L, any(), any()) } + } + + @Test + fun `Test refresh handles error`() = runTest { + val viewModel = getViewModel() + coEvery { repository.getConversation(any(), any(), any()) } throws Exception("Error") + + viewModel.uiState.value.loadingState.onRefresh() + + assertNotNull(viewModel.uiState.value.loadingState.snackbarMessage) + assertFalse(viewModel.uiState.value.loadingState.isRefreshing) + } + + @Test + fun `Test snackbar dismiss clears message`() = runTest { + coEvery { repository.getConversation(any(), any(), any()) } throws Exception("Error") + + val viewModel = getViewModel() + assertNotNull(viewModel.uiState.value.loadingState.snackbarMessage) + + viewModel.uiState.value.loadingState.onSnackbarDismiss() + + assertEquals(null, viewModel.uiState.value.loadingState.snackbarMessage) + } + + @Test + fun `Test attachment picker visibility toggle`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.replyState?.onShowAttachmentPickerChanged?.invoke(true) + + assertTrue(viewModel.uiState.value.replyState?.showAttachmentPicker == true) + } + + @Test + fun `Test attachments change updates reply state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.replyState?.onAttachmentsChanged?.invoke(emptyList()) + + assertEquals(0, viewModel.uiState.value.replyState?.attachments?.size) + } + + @Test + fun `Test exit confirmation dialog visibility toggle`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.replyState?.updateShowExitConfirmationDialog?.invoke(true) + + assertTrue(viewModel.uiState.value.replyState?.showExitConfirmationDialog == true) + } + + @Test + fun `Test invalid parameters set error state`() = runTest { + every { savedStateHandle.get(any()) } returns null + + val viewModel = getViewModel() + + assertTrue(viewModel.uiState.value.loadingState.isError) + } + + @Test + fun `Test load error sets error state`() = runTest { + coEvery { repository.getConversation(any(), any(), any()) } throws Exception("Error") + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + assertNotNull(viewModel.uiState.value.loadingState.snackbarMessage) + } + + @Test + fun `Test conversation with attachments are mapped`() = runTest { + val conversationWithAttachment = testConversation.copy( + messages = listOf( + Message( + id = 1L, + authorId = 1L, + body = "Message with attachment", + createdAt = "2025-01-01T00:00:00Z", + attachments = arrayListOf( + Attachment( + id = 1L, + displayName = "test.pdf", + url = "http://example.com/test.pdf", + contentType = "application/pdf" + ) + ) + ) + ) + ) + coEvery { repository.getConversation(any(), any(), any()) } returns conversationWithAttachment + + val viewModel = getViewModel() + + val item = viewModel.uiState.value.items.first() + assertEquals(1, item.attachments.size) + assertEquals("test.pdf", item.attachments.first().name) + } + + @Test + fun `Test course announcement loads correctly`() = runTest { + every { savedStateHandle.get("type") } returns HorizonInboxItemType.CourseNotification.navigationValue + + val viewModel = getViewModel() + + assertEquals("Test Announcement", viewModel.uiState.value.title) + assertNotNull(viewModel.uiState.value.titleIcon) + coVerify { repository.getAnnouncement(1L, 1L, false) } + coVerify { repository.getAnnouncementTopic(1L, 1L, false) } + } + + private fun getViewModel(): HorizonInboxDetailsViewModel { + return HorizonInboxDetailsViewModel( + context, + repository, + workManager, + fileDownloadProgressDao, + savedStateHandle, + inboxEventHandler, + dashboardEventHandler + ) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/list/HorizonInboxListRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/list/HorizonInboxListRepositoryTest.kt new file mode 100644 index 0000000000..a0059e4117 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/list/HorizonInboxListRepositoryTest.kt @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.inbox.list + +import com.instructure.canvasapi2.apis.AccountNotificationAPI +import com.instructure.canvasapi2.apis.AnnouncementAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.InboxApi +import com.instructure.canvasapi2.apis.RecipientAPI +import com.instructure.canvasapi2.models.AccountNotification +import com.instructure.canvasapi2.models.Conversation +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Recipient +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class HorizonInboxListRepositoryTest { + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val inboxApi: InboxApi.InboxInterface = mockk(relaxed = true) + private val recipientsApi: RecipientAPI.RecipientInterface = mockk(relaxed = true) + private val announcementsApi: AnnouncementAPI.AnnouncementInterface = mockk(relaxed = true) + private val accountNotificationApi: AccountNotificationAPI.AccountNotificationInterface = mockk(relaxed = true) + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + + private val userId = 1L + + @Before + fun setup() { + every { apiPrefs.user } returns User(id = userId, name = "Test User") + } + + @Test + fun `Test successful conversations retrieval`() = runTest { + val conversations = listOf( + Conversation(id = 1L, subject = "Conversation 1", lastMessage = "Message 1"), + Conversation(id = 2L, subject = "Conversation 2", lastMessage = "Message 2") + ) + coEvery { inboxApi.getConversations(any(), any()) } returns DataResult.Success(conversations) + + val result = getRepository().getConversations(forceNetwork = false) + + assertEquals(2, result.size) + assertEquals(conversations, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed conversations retrieval throws exception`() = runTest { + coEvery { inboxApi.getConversations(any(), any()) } returns DataResult.Fail() + + getRepository().getConversations(forceNetwork = false) + } + + @Test + fun `Test successful recipients retrieval filters by person type`() = runTest { + val recipients = listOf( + Recipient(stringId = "1", name = "Person 1"), + Recipient(stringId = "3", name = "Person 2") + ) + coEvery { recipientsApi.getFirstPageRecipientList(any(), any(), any()) } returns DataResult.Success(recipients) + + val result = getRepository().getRecipients("query", false) + + assertEquals(2, result.size) + assertTrue(result.all { it.recipientType == Recipient.Type.Person }) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed recipients retrieval throws exception`() = runTest { + coEvery { recipientsApi.getFirstPageRecipientList(any(), any(), any()) } returns DataResult.Fail() + + getRepository().getRecipients("query", false) + } + + @Test + fun `Test successful course announcements retrieval`() = runTest { + val course = Course(id = 1L, name = "Course 1",) + val announcement = DiscussionTopicHeader(id = 1L, title = "Announcement 1", contextCode = "course_1") + + coEvery { courseApi.getFirstPageCoursesInbox(any()) } returns DataResult.Success(listOf(course)) + coEvery { announcementsApi.getFirstPageAnnouncements(any(), params = any()) } returns + DataResult.Success(listOf(announcement)) + + val result = getRepository().getCourseAnnouncements(false) + + assertEquals(1, result.size) + assertEquals(course, result[0].first) + assertEquals(announcement, result[0].second) + } + + @Test + fun `Test course announcements with no courses returns empty list`() = runTest { + coEvery { courseApi.getFirstPageCoursesInbox(any()) } returns DataResult.Success(emptyList()) + + val result = getRepository().getCourseAnnouncements(false) + + assertTrue(result.isEmpty()) + } + + @Test + fun `Test successful account announcements retrieval`() = runTest { + val notifications = listOf( + AccountNotification(id = 1L, subject = "Notification 1"), + AccountNotification(id = 2L, subject = "Notification 2") + ) + coEvery { accountNotificationApi.getAccountNotifications(any(), any(), any()) } returns + DataResult.Success(notifications) + + val result = getRepository().getAccountAnnouncements(false) + + assertEquals(2, result.size) + assertEquals(notifications, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed account announcements retrieval throws exception`() = runTest { + coEvery { accountNotificationApi.getAccountNotifications(any(), any(), any()) } returns DataResult.Fail() + + getRepository().getAccountAnnouncements(false) + } + + @Test + fun `Test conversations scope filter`() = runTest { + val conversations = listOf(Conversation(id = 1L, subject = "Test")) + coEvery { inboxApi.getConversations(any(), any()) } returns DataResult.Success(conversations) + + getRepository().getConversations(InboxApi.Scope.SENT, false) + + coEvery { inboxApi.getConversations("sent", any()) } + } + + private fun getRepository(): HorizonInboxListRepository { + return HorizonInboxListRepository( + apiPrefs, + inboxApi, + recipientsApi, + announcementsApi, + accountNotificationApi, + courseApi + ) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/list/HorizonInboxListViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/list/HorizonInboxListViewModelTest.kt new file mode 100644 index 0000000000..55d322a627 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/inbox/list/HorizonInboxListViewModelTest.kt @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.inbox.list + +import android.content.Context +import com.instructure.canvasapi2.apis.InboxApi +import com.instructure.canvasapi2.models.Conversation +import com.instructure.canvasapi2.models.Recipient +import com.instructure.horizon.features.inbox.InboxEvent +import com.instructure.horizon.features.inbox.InboxEventHandler +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class HorizonInboxListViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: HorizonInboxListRepository = mockk(relaxed = true) + private val inboxEventHandler: InboxEventHandler = InboxEventHandler() + private val testDispatcher = UnconfinedTestDispatcher() + + private val testConversations = listOf( + Conversation(id = 1L, subject = "Test 1", lastMessage = "Message 1"), + Conversation(id = 2L, subject = "Test 2", lastMessage = "Message 2") + ) + + private val testRecipients = listOf( + Recipient(stringId = "1", name = "Recipient 1"), + Recipient(stringId = "2", name = "Recipient 2") + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + coEvery { repository.getConversations(any(), any()) } returns testConversations + coEvery { repository.getRecipients(any(), any()) } returns testRecipients + coEvery { repository.getCourseAnnouncements(any()) } returns emptyList() + coEvery { repository.getAccountAnnouncements(any()) } returns emptyList() + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test data loads successfully`() = runTest { + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + coVerify { repository.getConversations(InboxApi.Scope.INBOX, false) } + } + + @Test + fun `Test conversations are loaded`() = runTest { + val viewModel = getViewModel() + + coVerify { repository.getConversations(InboxApi.Scope.INBOX, false) } + } + + @Test + fun `Test failed data load sets error state`() = runTest { + coEvery { repository.getConversations(any(), any()) } throws Exception("Network error") + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + } + + @Test + fun `Test scope filter change loads correct data`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateScopeFilter(HorizonInboxScope.Sent) + + coVerify { repository.getConversations(InboxApi.Scope.SENT, any()) } + } + + @Test + fun `Test unread scope filter`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateScopeFilter(HorizonInboxScope.Unread) + + coVerify { repository.getConversations(InboxApi.Scope.UNREAD, any()) } + } + + @Test + fun `Test announcements scope loads announcements`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateScopeFilter(HorizonInboxScope.Announcements) + + coVerify { repository.getCourseAnnouncements(any()) } + coVerify { repository.getAccountAnnouncements(any()) } + } + + @Test + fun `Test recipient selected adds to list`() = runTest { + val viewModel = getViewModel() + + val recipient = Recipient(stringId = "3", name = "New Recipient") + viewModel.uiState.value.onRecipientSelected(recipient) + + assertTrue(viewModel.uiState.value.selectedRecipients.contains(recipient)) + } + + @Test + fun `Test recipient removed from list`() = runTest { + val viewModel = getViewModel() + + val recipient = Recipient(stringId = "3", name = "Recipient") + viewModel.uiState.value.onRecipientSelected(recipient) + viewModel.uiState.value.onRecipientRemoved(recipient) + + assertFalse(viewModel.uiState.value.selectedRecipients.contains(recipient)) + } + + @Test + fun `Test refresh reloads data`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.loadingState.onRefresh() + + coVerify(atLeast = 2) { repository.getConversations(any(), any()) } + } + + @Test + fun `RefreshRequested event triggers refresh and reloads conversations`() = runTest { + val viewModel = getViewModel() + + val updatedConversations = listOf( + Conversation(id = 3L, subject = "Updated 1", lastMessage = "New Message 1"), + Conversation(id = 4L, subject = "Updated 2", lastMessage = "New Message 2") + ) + coEvery { repository.getConversations(any(), any()) } returns updatedConversations + + inboxEventHandler.postEvent(InboxEvent.RefreshRequested) + testDispatcher.scheduler.advanceUntilIdle() + + coVerify(atLeast = 2) { repository.getConversations(any(), any()) } + } + + @Test + fun `AnnouncementRead event triggers refresh`() = runTest { + val viewModel = getViewModel() + + inboxEventHandler.postEvent(InboxEvent.AnnouncementRead) + testDispatcher.scheduler.advanceUntilIdle() + + coVerify(atLeast = 2) { repository.getConversations(any(), any()) } + } + + @Test + fun `ConversationCreated event triggers refresh and shows snackbar`() = runTest { + val viewModel = getViewModel() + + val testMessage = "Conversation created successfully" + inboxEventHandler.postEvent(InboxEvent.ConversationCreated(testMessage)) + testDispatcher.scheduler.advanceUntilIdle() + + coVerify(atLeast = 2) { repository.getConversations(any(), any()) } + } + + private fun getViewModel(): HorizonInboxListViewModel { + return HorizonInboxListViewModel(context, repository, inboxEventHandler) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/learn/LearnRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/learn/LearnRepositoryTest.kt new file mode 100644 index 0000000000..bce5532773 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/learn/LearnRepositoryTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.learn + +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithModuleItemDurations +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.GetProgramsManager +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Program +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.journey.type.ProgramVariantType +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class LearnRepositoryTest { + private val horizonGetCoursesManager: HorizonGetCoursesManager = mockk(relaxed = true) + private val getProgramsManager: GetProgramsManager = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + + private val userId = 1L + + @Before + fun setup() { + every { apiPrefs.user } returns User(id = userId, name = "Test User") + } + + @Test + fun `Test successful courses with progress retrieval`() = runTest { + val courses = listOf( + CourseWithProgress( + courseId = 1L, + courseName = "Course 1", + courseSyllabus = "", + progress = 50.0 + ), + CourseWithProgress( + courseId = 2L, + courseName = "Course 2", + courseSyllabus = "", + progress = 75.0 + ) + ) + coEvery { horizonGetCoursesManager.getCoursesWithProgress(userId, false) } returns DataResult.Success(courses) + + val result = getRepository().getCoursesWithProgress(false) + + assertEquals(2, result.size) + assertEquals(courses, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed courses retrieval throws exception`() = runTest { + coEvery { horizonGetCoursesManager.getCoursesWithProgress(userId, false) } returns DataResult.Fail() + + getRepository().getCoursesWithProgress(false) + } + + @Test + fun `Test successful programs retrieval`() = runTest { + val programs = listOf( + Program( + id = "1", + name = "Program 1", + description = "Program 1 Description", + sortedRequirements = emptyList(), + startDate = null, + endDate = null, + variant = ProgramVariantType.LINEAR, + ), + Program( + id = "2", + name = "Program 2", + description = "Program 2 Description", + sortedRequirements = emptyList(), + startDate = null, + endDate = null, + variant = ProgramVariantType.NON_LINEAR, + ) + ) + coEvery { getProgramsManager.getPrograms(false) } returns programs + + val result = getRepository().getPrograms(false) + + assertEquals(2, result.size) + assertEquals(programs, result) + } + + @Test + fun `Test successful get courses by id`() = runTest { + val courseIds = listOf(1L, 2L) + val course1 = CourseWithModuleItemDurations(courseId = 1L, courseName = "Course 1") + val course2 = CourseWithModuleItemDurations(courseId = 2L, courseName = "Course 2") + + coEvery { horizonGetCoursesManager.getProgramCourses(1L, false) } returns DataResult.Success(course1) + coEvery { horizonGetCoursesManager.getProgramCourses(2L, false) } returns DataResult.Success(course2) + + val result = getRepository().getCoursesById(courseIds, false) + + assertEquals(2, result.size) + assertEquals(course1, result[0]) + assertEquals(course2, result[1]) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed get courses by id throws exception`() = runTest { + val courseIds = listOf(1L) + coEvery { horizonGetCoursesManager.getProgramCourses(1L, false) } returns DataResult.Fail() + + getRepository().getCoursesById(courseIds, false) + } + + private fun getRepository(): LearnRepository { + return LearnRepository(horizonGetCoursesManager, getProgramsManager, apiPrefs) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/learn/LearnViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/learn/LearnViewModelTest.kt new file mode 100644 index 0000000000..a7b4af2c63 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/learn/LearnViewModelTest.kt @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.learn + +import android.content.Context +import androidx.lifecycle.SavedStateHandle +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithModuleItemDurations +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.journey.Program +import com.instructure.canvasapi2.managers.graphql.horizon.journey.ProgramRequirement +import com.instructure.journey.type.ProgramProgressCourseEnrollmentStatus +import com.instructure.journey.type.ProgramVariantType +import io.mockk.coEvery +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class LearnViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: LearnRepository = mockk(relaxed = true) + private val savedStateHandle: SavedStateHandle = SavedStateHandle() + private val learnEventHandler: LearnEventHandler = LearnEventHandler() + private val testDispatcher = UnconfinedTestDispatcher() + + private val testCourses = listOf( + CourseWithProgress( + courseId = 1L, + courseName = "Course 1", + courseSyllabus = "", + progress = 50.0 + ), + CourseWithProgress( + courseId = 2L, + courseName = "Course 2", + courseSyllabus = "", + progress = 75.0 + ), + CourseWithProgress( + courseId = 3L, + courseName = "Course 3", + courseSyllabus = "", + progress = 25.0 + ) + ) + + private val testProgram = Program( + id = "prog1", + name = "Program 1", + description = "Program 1 description", + sortedRequirements = listOf( + ProgramRequirement( + id = "req1", + progressId = "prog1", + courseId = 1L, + required = true, + progress = 2.0, + enrollmentStatus = ProgramProgressCourseEnrollmentStatus.ENROLLED + ) + ), + startDate = null, + endDate = null, + variant = ProgramVariantType.LINEAR, + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + coEvery { repository.getPrograms(any()) } returns listOf(testProgram) + coEvery { repository.getCoursesWithProgress(any()) } returns testCourses + coEvery { repository.getCoursesById(any(), any()) } returns listOf( + CourseWithModuleItemDurations(courseId = 1L, courseName = "Course 1") + ) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test data loads successfully`() = runTest { + val viewModel = getViewModel() + + assertFalse(viewModel.state.value.screenState.isLoading) + assertTrue(viewModel.state.value.learningItems.isNotEmpty()) + } + + @Test + fun `Test standalone courses are separated from program courses`() = runTest { + val viewModel = getViewModel() + + val learningItems = viewModel.state.value.learningItems + + assertTrue(learningItems.any { item -> + item is LearningItem.CourseItem && item.courseWithProgress.courseId == 2L + }) + assertTrue(learningItems.any { item -> + item is LearningItem.CourseItem && item.courseWithProgress.courseId == 3L + }) + } + + @Test + fun `Test program courses are grouped`() = runTest { + val viewModel = getViewModel() + + val learningItems = viewModel.state.value.learningItems + + assertTrue(learningItems.any { item -> + item is LearningItem.ProgramGroupItem && item.programName == "Program 1" + }) + } + + @Test + fun `Test failed data load sets error state`() = runTest { + coEvery { repository.getPrograms(any()) } throws Exception("Network error") + + val viewModel = getViewModel() + + assertFalse(viewModel.state.value.screenState.isLoading) + assertTrue(viewModel.state.value.screenState.isError) + } + + @Test + fun `Test empty courses list`() = runTest { + coEvery { repository.getCoursesWithProgress(any()) } returns emptyList() + coEvery { repository.getPrograms(any()) } returns emptyList() + + val viewModel = getViewModel() + + assertFalse(viewModel.state.value.screenState.isLoading) + assertTrue(viewModel.state.value.learningItems.isEmpty()) + } + + @Test + fun `Test learningItemId from saved state`() = runTest { + val savedStateWithId = SavedStateHandle(mapOf("learningItemId" to "testId")) + + val viewModel = LearnViewModel(context, repository, savedStateWithId, learnEventHandler) + + assertFalse(viewModel.state.value.screenState.isLoading) + } + + @Test + fun `RefreshRequested event triggers refresh and reloads data`() = runTest { + val viewModel = getViewModel() + + val updatedCourses = listOf( + CourseWithProgress(courseId = 4L, courseName = "Updated Course", courseSyllabus = "", progress = 90.0) + ) + coEvery { repository.getCoursesWithProgress(any()) } returns updatedCourses + + learnEventHandler.postEvent(LearnEvent.RefreshRequested) + testDispatcher.scheduler.advanceUntilIdle() + + assertTrue(viewModel.state.value.screenState.isRefreshing == false) + } + + @Test + fun `Multiple refresh events update state correctly`() = runTest { + val viewModel = getViewModel() + + learnEventHandler.postEvent(LearnEvent.RefreshRequested) + testDispatcher.scheduler.advanceUntilIdle() + + val secondCourses = listOf( + CourseWithProgress(courseId = 5L, courseName = "Second Update", courseSyllabus = "", progress = 100.0) + ) + coEvery { repository.getCoursesWithProgress(any()) } returns secondCourses + coEvery { repository.getPrograms(any()) } returns emptyList() + + learnEventHandler.postEvent(LearnEvent.RefreshRequested) + testDispatcher.scheduler.advanceUntilIdle() + + assertFalse(viewModel.state.value.screenState.isRefreshing) + } + + private fun getViewModel(): LearnViewModel { + return LearnViewModel(context, repository, savedStateHandle, learnEventHandler) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceRepositoryTest.kt new file mode 100644 index 0000000000..de0f7890dc --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceRepositoryTest.kt @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.moduleitemsequence + +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCommentsManager +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleItemSequence +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.Before +import org.junit.Test + +class ModuleItemSequenceRepositoryTest { + private val moduleApi: ModuleAPI.ModuleInterface = mockk(relaxed = true) + private val assignmentApi: AssignmentAPI.AssignmentInterface = mockk(relaxed = true) + private val horizonGetCommentsManager: HorizonGetCommentsManager = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + + private val userId = 1L + private val courseId = 1L + + @Before + fun setup() { + every { apiPrefs.user } returns User(id = userId, name = "Test User") + } + + @Test + fun `Test successful module item sequence retrieval`() = runTest { + val sequence = ModuleItemSequence() + coEvery { moduleApi.getModuleItemSequence(any(), any(), any(), any(), any()) } returns + DataResult.Success(sequence) + + val result = getRepository().getModuleItemSequence(courseId, "Assignment", "123") + + assertEquals(sequence, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed module item sequence retrieval throws exception`() = runTest { + coEvery { moduleApi.getModuleItemSequence(any(), any(), any(), any(), any()) } returns DataResult.Fail() + + getRepository().getModuleItemSequence(courseId, "Assignment", "123") + } + + @Test + fun `Test successful modules with items retrieval`() = runTest { + val module = ModuleObject(id = 1L, name = "Module 1", itemCount = 2, items = listOf( + ModuleItem(id = 1L, title = "Item 1"), + ModuleItem(id = 2L, title = "Item 2") + )) + coEvery { moduleApi.getFirstPageModulesWithItems(any(), any(), any(), any()) } returns + DataResult.Success(listOf(module)) + + val result = getRepository().getModulesWithItems(courseId, false) + + assertEquals(1, result.size) + assertEquals(module, result[0]) + } + + @Test + fun `Test modules with incomplete items fetches all items`() = runTest { + val incompleteModule = ModuleObject(id = 1L, name = "Module 1", itemCount = 3, items = listOf( + ModuleItem(id = 1L, title = "Item 1") + )) + val allItems = listOf( + ModuleItem(id = 1L, title = "Item 1"), + ModuleItem(id = 2L, title = "Item 2"), + ModuleItem(id = 3L, title = "Item 3") + ) + + coEvery { moduleApi.getFirstPageModulesWithItems(any(), any(), any(), any()) } returns + DataResult.Success(listOf(incompleteModule)) + coEvery { moduleApi.getFirstPageModuleItems(any(), any(), any(), any(), any()) } returns + DataResult.Success(allItems) + + val result = getRepository().getModulesWithItems(courseId, false) + + assertEquals(1, result.size) + assertEquals(3, result[0].items.size) + } + + @Test + fun `Test successful module item retrieval`() = runTest { + val moduleItem = ModuleItem(id = 1L, title = "Item 1", moduleId = 1L) + coEvery { moduleApi.getModuleItem(any(), any(), any(), any(), any()) } returns + DataResult.Success(moduleItem) + + val result = getRepository().getModuleItem(courseId, 1L, 1L) + + assertEquals(moduleItem, result) + } + + @Test + fun `Test mark as done success`() = runTest { + val moduleItem = ModuleItem(id = 1L, moduleId = 1L) + coEvery { moduleApi.markModuleItemAsDone(any(), any(), any(), any(), any()) } returns + DataResult.Success("".toResponseBody()) + + val result = getRepository().markAsDone(courseId, moduleItem) + + assertTrue(result is DataResult.Success) + } + + @Test + fun `Test mark as not done success`() = runTest { + val moduleItem = ModuleItem(id = 1L, moduleId = 1L) + coEvery { moduleApi.markModuleItemAsNotDone(any(), any(), any(), any(), any()) } returns + DataResult.Success("".toResponseBody()) + + val result = getRepository().markAsNotDone(courseId, moduleItem) + + assertTrue(result is DataResult.Success) + } + + @Test + fun `Test mark as read`() = runTest { + coEvery { moduleApi.markModuleItemRead(any(), any(), any(), any(), any()) } returns + DataResult.Success("".toResponseBody()) + + getRepository().markAsRead(courseId, 1L, 1L) + + coVerify { moduleApi.markModuleItemRead(any(), courseId, 1L, 1L, any()) } + } + + @Test + fun `Test successful assignment retrieval`() = runTest { + val assignment = Assignment(id = 1L, name = "Assignment 1") + coEvery { assignmentApi.getAssignmentWithHistory(any(), any(), any()) } returns + DataResult.Success(assignment) + + val result = getRepository().getAssignment(1L, courseId, false) + + assertEquals(assignment, result) + } + + @Test + fun `Test has unread comments returns true when count greater than zero`() = runTest { + coEvery { horizonGetCommentsManager.getUnreadCommentsCount(any(), any(), any()) } returns 5 + + val result = getRepository().hasUnreadComments(1L, false) + + assertTrue(result) + } + + @Test + fun `Test has unread comments returns false when count is zero`() = runTest { + coEvery { horizonGetCommentsManager.getUnreadCommentsCount(any(), any(), any()) } returns 0 + + val result = getRepository().hasUnreadComments(1L, false) + + assertFalse(result) + } + + @Test + fun `Test has unread comments returns false when assignment id is null`() = runTest { + val result = getRepository().hasUnreadComments(null, false) + + assertFalse(result) + } + + private fun getRepository(): ModuleItemSequenceRepository { + return ModuleItemSequenceRepository(moduleApi, assignmentApi, horizonGetCommentsManager, apiPrefs) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceViewModelTest.kt new file mode 100644 index 0000000000..46785f31b6 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceViewModelTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.moduleitemsequence + +import android.content.Context +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.toRoute +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleItemSequence +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.horizon.features.aiassistant.common.AiAssistContextProvider +import com.instructure.horizon.features.dashboard.DashboardEventHandler +import com.instructure.horizon.features.learn.LearnEventHandler +import com.instructure.horizon.horizonui.organisms.cards.ModuleItemCardStateMapper +import com.instructure.horizon.navigation.MainNavigationRoute +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class ModuleItemSequenceViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: ModuleItemSequenceRepository = mockk(relaxed = true) + private val moduleItemCardStateMapper: ModuleItemCardStateMapper = mockk(relaxed = true) + private val aiAssistContextProvider: AiAssistContextProvider = mockk(relaxed = true) + private val dashboardEventHandler: DashboardEventHandler = DashboardEventHandler() + private val learnEventHandler: LearnEventHandler = LearnEventHandler() + private val testDispatcher = UnconfinedTestDispatcher() + private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) + + private val courseId = 1L + private val moduleItemId = 100L + + private val testModuleItem = ModuleItem( + id = moduleItemId, + title = "Test Item", + moduleId = 1L, + type = "Assignment" + ) + + private val testModule = ModuleObject( + id = 1L, + name = "Test Module", + items = listOf(testModuleItem) + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + mockkStatic("androidx.navigation.SavedStateHandleKt") + every { savedStateHandle.toRoute() } returns MainNavigationRoute.ModuleItemSequence( + courseId = courseId, + moduleItemId = moduleItemId, + moduleItemAssetType = null, + moduleItemAssetId = null + ) + coEvery { repository.getModuleItemSequence(any(), any(), any()) } returns ModuleItemSequence() + coEvery { repository.getModulesWithItems(any(), any()) } returns listOf(testModule) + coEvery { repository.getModuleItem(any(), any(), any()) } returns testModuleItem + coEvery { repository.getAssignment(any(), any(), any()) } returns Assignment(id = 1L) + coEvery { repository.hasUnreadComments(any(), any()) } returns false + coEvery { moduleItemCardStateMapper.mapModuleItemToCardState(any(), any()) } returns mockk(relaxed = true) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test data loads with moduleItemId`() = runTest { + val viewModel = getViewModel(savedStateHandle) + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + coVerify { repository.getModulesWithItems(courseId, any()) } + } + + @Test + fun `Test module items are loaded`() = runTest { + val viewModel = getViewModel(savedStateHandle) + + assertNotNull(viewModel.uiState.value) + coVerify { repository.getModulesWithItems(courseId, true) } + } + + @Test + fun `Test failed data load sets error state`() = runTest { + coEvery { repository.getModulesWithItems(any(), any()) } throws Exception("Network error") + + val viewModel = getViewModel(savedStateHandle) + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + } + + @Test + fun `Test assignment is fetched for module item`() = runTest { + coEvery { repository.getAssignment(any(), any(), any()) } returns Assignment(id = 123L, name = "Test Assignment") + + val viewModel = getViewModel(savedStateHandle) + + coVerify { repository.getAssignment(any(), courseId, any()) } + } + + @Test + fun `Test unread comments check is performed`() = runTest { + val assignment = Assignment(id = 123L) + coEvery { repository.getAssignment(any(), any(), any()) } returns assignment + + val viewModel = getViewModel(savedStateHandle) + + coVerify { repository.hasUnreadComments(123L, any()) } + } + + private fun getViewModel(savedStateHandle: SavedStateHandle): ModuleItemSequenceViewModel { + return ModuleItemSequenceViewModel( + context, + repository, + moduleItemCardStateMapper, + aiAssistContextProvider, + savedStateHandle, + dashboardEventHandler, + learnEventHandler + ) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentRepositoryTest.kt new file mode 100644 index 0000000000..5ba75ef571 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentRepositoryTest.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.moduleitemsequence.content.assessment + +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.LaunchDefinitionsAPI +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.LTITool +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +class AssessmentRepositoryTest { + private val assignmentApi: AssignmentAPI.AssignmentInterface = mockk(relaxed = true) + private val oAuthInterface: OAuthAPI.OAuthInterface = mockk(relaxed = true) + private val launchDefinitionsApi: LaunchDefinitionsAPI.LaunchDefinitionsInterface = mockk(relaxed = true) + + private lateinit var repository: AssessmentRepository + + private val testAssignment = Assignment( + id = 1L, + name = "Test Quiz", + courseId = 100L, + url = "https://example.com/quiz/1" + ) + + private val testLTITool = LTITool( + url = "https://lti.example.com/tool", + id = 1 + ) + + @Before + fun setup() { + repository = AssessmentRepository(assignmentApi, oAuthInterface, launchDefinitionsApi) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `getAssignment returns assignment successfully`() = runTest { + coEvery { assignmentApi.getAssignmentWithHistory(any(), any(), any()) } returns DataResult.Success(testAssignment) + + val result = repository.getAssignment(assignmentId = 1L, courseId = 100L, forceNetwork = false) + + assertEquals("Test Quiz", result.name) + assertEquals(1L, result.id) + coVerify { assignmentApi.getAssignmentWithHistory(100L, 1L, any()) } + } + + @Test + fun `getAssignment with forceNetwork true`() = runTest { + coEvery { assignmentApi.getAssignmentWithHistory(any(), any(), any()) } returns DataResult.Success(testAssignment) + + repository.getAssignment(assignmentId = 1L, courseId = 100L, forceNetwork = true) + + coVerify { assignmentApi.getAssignmentWithHistory(any(), any(), match { it.isForceReadFromNetwork }) } + } + + @Test + fun `getAssignment with forceNetwork false`() = runTest { + coEvery { assignmentApi.getAssignmentWithHistory(any(), any(), any()) } returns DataResult.Success(testAssignment) + + repository.getAssignment(assignmentId = 1L, courseId = 100L, forceNetwork = false) + + coVerify { assignmentApi.getAssignmentWithHistory(any(), any(), match { !it.isForceReadFromNetwork }) } + } + + @Test + fun `authenticateUrl returns authenticated URL for LTI tool`() = runTest { + val session = AuthenticatedSession(sessionUrl = "https://authenticated.lti.url") + coEvery { launchDefinitionsApi.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Success(testLTITool) + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Success(session) + + val result = repository.authenticateUrl("https://example.com/quiz") + + assertEquals("https://authenticated.lti.url", result) + coVerify { launchDefinitionsApi.getLtiFromAuthenticationUrl("https://example.com/quiz", any()) } + coVerify { oAuthInterface.getAuthenticatedSession("https://lti.example.com/tool", any()) } + } + + @Test + fun `authenticateUrl returns original URL when LTI tool URL is null`() = runTest { + val ltiToolWithoutUrl = LTITool(url = null, id = 1) + coEvery { launchDefinitionsApi.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Success(ltiToolWithoutUrl) + + val result = repository.authenticateUrl("https://example.com/quiz") + + assertEquals("https://example.com/quiz", result) + } + + @Test + fun `authenticateUrl returns original URL when session URL is null`() = runTest { + val session = AuthenticatedSession(sessionUrl = "https://example.com/quiz/authenticated") + coEvery { launchDefinitionsApi.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Success(testLTITool) + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Success(session) + + val result = repository.authenticateUrl("https://example.com/quiz") + + assertEquals("https://example.com/quiz/authenticated", result) + } + + @Test + fun `authenticateUrl returns original URL when authentication fails`() = runTest { + coEvery { launchDefinitionsApi.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Success(testLTITool) + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Fail() + + val result = repository.authenticateUrl("https://example.com/quiz") + + assertEquals("https://example.com/quiz", result) + } + + @Test + fun `authenticateUrl always uses forceNetwork`() = runTest { + val session = AuthenticatedSession(sessionUrl = "https://authenticated.url") + coEvery { launchDefinitionsApi.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Success(testLTITool) + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Success(session) + + repository.authenticateUrl("https://example.com") + + coVerify { launchDefinitionsApi.getLtiFromAuthenticationUrl(any(), match { it.isForceReadFromNetwork }) } + coVerify { oAuthInterface.getAuthenticatedSession(any(), match { it.isForceReadFromNetwork }) } + } + + @Test + fun `getAssignment with different course and assignment IDs`() = runTest { + coEvery { assignmentApi.getAssignmentWithHistory(any(), any(), any()) } returns DataResult.Success(testAssignment) + + repository.getAssignment(assignmentId = 99L, courseId = 200L, forceNetwork = false) + + coVerify { assignmentApi.getAssignmentWithHistory(200L, 99L, any()) } + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentViewModelTest.kt new file mode 100644 index 0000000000..2ededc5f96 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assessment/AssessmentViewModelTest.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.moduleitemsequence.content.assessment + +import androidx.lifecycle.SavedStateHandle +import com.instructure.canvasapi2.models.Assignment +import com.instructure.horizon.features.moduleitemsequence.ModuleItemContent +import com.instructure.pandautils.utils.Const +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AssessmentViewModelTest { + private val repository: AssessmentRepository = mockk(relaxed = true) + private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val assignmentId = 1L + private val courseId = 100L + private val testAssignment = Assignment( + id = assignmentId, + name = "Test Quiz", + url = "https://example.com/quiz/1" + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + every { savedStateHandle.get(ModuleItemContent.Assignment.ASSIGNMENT_ID) } returns assignmentId + every { savedStateHandle.get(Const.COURSE_ID) } returns courseId + coEvery { repository.getAssignment(any(), any(), any()) } returns testAssignment + coEvery { repository.authenticateUrl(any()) } returns "https://authenticated.url" + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel loads assignment data`() = runTest { + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + assertEquals("Test Quiz", viewModel.uiState.value.assessmentName) + coVerify { repository.getAssignment(assignmentId, courseId, false) } + } + + @Test + fun `Test start quiz clicked shows dialog and loads URL`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onStartQuizClicked() + + assertTrue(viewModel.uiState.value.showAssessmentDialog) + assertEquals("https://authenticated.url", viewModel.uiState.value.urlToLoad) + viewModel.uiState.value.onAssessmentLoaded() + assertFalse(viewModel.uiState.value.assessmentLoading) + coVerify { repository.authenticateUrl("https://example.com/quiz/1") } + } + + @Test + fun `Test start quiz with authentication error`() = runTest { + coEvery { repository.authenticateUrl(any()) } throws Exception("Auth error") + + val viewModel = getViewModel() + + viewModel.uiState.value.onStartQuizClicked() + + assertTrue(viewModel.uiState.value.showAssessmentDialog) + assertFalse(viewModel.uiState.value.assessmentLoading) + } + + @Test + fun `Test assessment closed clears URL and dialog`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.onStartQuizClicked() + + viewModel.uiState.value.onAssessmentClosed() + + assertNull(viewModel.uiState.value.urlToLoad) + assertFalse(viewModel.uiState.value.showAssessmentDialog) + } + + @Test + fun `Test assessment loaded clears loading state`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.onStartQuizClicked() + + viewModel.uiState.value.onAssessmentLoaded() + + assertFalse(viewModel.uiState.value.assessmentLoading) + } + + @Test + fun `Test assessment completion starts loading`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onAssessmentCompletion() + + assertTrue(viewModel.uiState.value.assessmentCompletionLoading) + } + + @Test + fun `Test assessment completion finishes after delay`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onAssessmentCompletion() + assertTrue(viewModel.uiState.value.assessmentCompletionLoading) + + advanceTimeBy(15100) + + assertFalse(viewModel.uiState.value.assessmentCompletionLoading) + } + + @Test + fun `Test load error sets error state`() = runTest { + coEvery { repository.getAssignment(any(), any(), any()) } throws Exception("Error") + + val viewModel = getViewModel() + + assertTrue(viewModel.uiState.value.loadingState.isError) + } + + @Test + fun `Test start quiz with null assessment URL`() = runTest { + coEvery { repository.getAssignment(any(), any(), any()) } returns testAssignment.copy(url = null) + + val viewModel = getViewModel() + + viewModel.uiState.value.onStartQuizClicked() + + assertTrue(viewModel.uiState.value.showAssessmentDialog) + assertNull(viewModel.uiState.value.urlToLoad) + assertFalse(viewModel.uiState.value.assessmentLoading) + } + + @Test + fun `Test UI state contains all callbacks`() = runTest { + val viewModel = getViewModel() + + assertNotNull(viewModel.uiState.value.onAssessmentClosed) + assertNotNull(viewModel.uiState.value.onStartQuizClicked) + assertNotNull(viewModel.uiState.value.onAssessmentCompletion) + assertNotNull(viewModel.uiState.value.onAssessmentLoaded) + } + + private fun getViewModel(): AssessmentViewModel { + return AssessmentViewModel(repository, savedStateHandle) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsRepositoryTest.kt new file mode 100644 index 0000000000..6f06cfa94d --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsRepositoryTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.moduleitemsequence.content.assignment + +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCommentsManager +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class AssignmentDetailsRepositoryTest { + private val assignmentApi: AssignmentAPI.AssignmentInterface = mockk(relaxed = true) + private val oAuthInterface: OAuthAPI.OAuthInterface = mockk(relaxed = true) + private val horizonGetCommentsManager: HorizonGetCommentsManager = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + + private val userId = 1L + private val courseId = 1L + private val assignmentId = 1L + + @Before + fun setup() { + every { apiPrefs.user } returns User(id = userId, name = "Test User") + } + + @Test + fun `Test successful assignment retrieval`() = runTest { + val assignment = Assignment(id = assignmentId, name = "Test Assignment", pointsPossible = 100.0) + coEvery { assignmentApi.getAssignmentWithHistory(courseId, assignmentId, any()) } returns + DataResult.Success(assignment) + + val result = getRepository().getAssignment(assignmentId, courseId, false) + + assertEquals(assignment, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed assignment retrieval throws exception`() = runTest { + coEvery { assignmentApi.getAssignmentWithHistory(courseId, assignmentId, any()) } returns + DataResult.Fail() + + getRepository().getAssignment(assignmentId, courseId, false) + } + + @Test + fun `Test successful URL authentication`() = runTest { + val originalUrl = "https://example.com/file" + val authenticatedUrl = "https://example.com/file?session=xyz" + val session = AuthenticatedSession(sessionUrl = authenticatedUrl) + + coEvery { oAuthInterface.getAuthenticatedSession(originalUrl, any()) } returns + DataResult.Success(session) + + val result = getRepository().authenticateUrl(originalUrl) + + assertEquals(authenticatedUrl, result) + } + + @Test + fun `Test URL authentication fallback on failure`() = runTest { + val originalUrl = "https://example.com/file" + coEvery { oAuthInterface.getAuthenticatedSession(originalUrl, any()) } returns DataResult.Fail() + + val result = getRepository().authenticateUrl(originalUrl) + + assertEquals(originalUrl, result) + } + + @Test + fun `Test URL authentication fallback on null session`() = runTest { + val originalUrl = "https://example.com/file" + coEvery { oAuthInterface.getAuthenticatedSession(originalUrl, any()) } returns DataResult.Fail() + + val result = getRepository().authenticateUrl(originalUrl) + + assertEquals(originalUrl, result) + } + + @Test + fun `Test has unread comments returns true when count greater than zero`() = runTest { + coEvery { horizonGetCommentsManager.getUnreadCommentsCount(assignmentId, userId, false) } returns 3 + + val result = getRepository().hasUnreadComments(assignmentId, false) + + assertTrue(result) + } + + @Test + fun `Test has unread comments returns false when count is zero`() = runTest { + coEvery { horizonGetCommentsManager.getUnreadCommentsCount(assignmentId, userId, false) } returns 0 + + val result = getRepository().hasUnreadComments(assignmentId, false) + + assertFalse(result) + } + + @Test + fun `Test force network parameter is passed correctly`() = runTest { + val assignment = Assignment(id = assignmentId, name = "Test Assignment") + coEvery { assignmentApi.getAssignmentWithHistory(courseId, assignmentId, any()) } returns + DataResult.Success(assignment) + + getRepository().getAssignment(assignmentId, courseId, true) + + coEvery { assignmentApi.getAssignmentWithHistory(courseId, assignmentId, match { it.isForceReadFromNetwork }) } + } + + private fun getRepository(): AssignmentDetailsRepository { + return AssignmentDetailsRepository(assignmentApi, oAuthInterface, horizonGetCommentsManager, apiPrefs) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsViewModelTest.kt new file mode 100644 index 0000000000..a9cdd1eb34 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/AssignmentDetailsViewModelTest.kt @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.moduleitemsequence.content.assignment + +import android.content.Context +import androidx.lifecycle.SavedStateHandle +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Submission +import com.instructure.horizon.features.aiassistant.common.AiAssistContextProvider +import com.instructure.horizon.features.moduleitemsequence.ModuleItemContent +import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.HtmlContentFormatter +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AssignmentDetailsViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: AssignmentDetailsRepository = mockk(relaxed = true) + private val htmlContentFormatter: HtmlContentFormatter = mockk(relaxed = true) + private val aiAssistContextProvider: AiAssistContextProvider = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val courseId = 1L + private val assignmentId = 100L + + private val testAssignment = Assignment( + id = assignmentId, + name = "Test Assignment", + pointsPossible = 100.0, + description = "Test description", + allowedAttempts = 3L + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + coEvery { repository.getAssignment(any(), any(), any()) } returns testAssignment + coEvery { repository.hasUnreadComments(any(), any()) } returns false + coEvery { repository.authenticateUrl(any()) } returns "https://authenticated.url" + coEvery { htmlContentFormatter.formatHtmlWithIframes(any(), any()) } returns "Formatted content" + coEvery { aiAssistContextProvider.aiAssistContext } returns mockk(relaxed = true) + coEvery { aiAssistContextProvider.aiAssistContext = any() } returns Unit + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test data loads with assignmentId and courseId`() = runTest { + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + coVerify { repository.getAssignment(assignmentId, courseId, false) } + } + + @Test + fun `Test assignment is loaded successfully`() = runTest { + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertNotNull(viewModel.assignmentFlow.value) + assertEquals(testAssignment, viewModel.assignmentFlow.value) + } + + @Test + fun `Test failed data load sets error state`() = runTest { + coEvery { repository.getAssignment(any(), any(), any()) } throws Exception("Network error") + + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + } + + @Test + fun `Test unread comments check is performed`() = runTest { + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + coVerify { repository.hasUnreadComments(assignmentId, false) } + } + + @Test + fun `Test unread comments flag is set correctly`() = runTest { + coEvery { repository.hasUnreadComments(any(), any()) } returns true + + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertTrue(viewModel.uiState.value.toolsBottomSheetUiState.hasUnreadComments) + } + + @Test + fun `Test assignment with no submission shows add submission`() = runTest { + val assignmentWithoutSubmission = testAssignment.copy(submission = null) + coEvery { repository.getAssignment(any(), any(), any()) } returns assignmentWithoutSubmission + + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertTrue(viewModel.uiState.value.showAddSubmission) + assertFalse(viewModel.uiState.value.showSubmissionDetails) + } + + @Test + fun `Test assignment with submission shows submission details`() = runTest { + val submission = Submission(attempt = 1L, workflowState = "submitted") + val assignmentWithSubmission = testAssignment.copy( + submission = submission + ) + coEvery { repository.getAssignment(any(), any(), any()) } returns assignmentWithSubmission + + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertTrue(viewModel.uiState.value.showSubmissionDetails) + } + + @Test + fun `Test LTI URL authentication is performed`() = runTest { + val ltiUrl = "https://lti.example.com/launch" + val testAssignmentWithLti = testAssignment.copy( + externalToolAttributes = mockk { + coEvery { url } returns ltiUrl + } + ) + coEvery { repository.getAssignment(any(), any(), any()) } returns testAssignmentWithLti + + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertEquals(ltiUrl, viewModel.uiState.value.ltiUrl) + } + + @Test + fun `Test HTML content formatting is applied to description`() = runTest { + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + coVerify { htmlContentFormatter.formatHtmlWithIframes(testAssignment.description.orEmpty(), courseId) } + assertEquals("Formatted content", viewModel.uiState.value.instructions) + } + + @Test + fun `Test attempt selector visibility for multiple attempts`() = runTest { + val assignmentWithMultipleAttempts = testAssignment.copy(allowedAttempts = 3L) + coEvery { repository.getAssignment(any(), any(), any()) } returns assignmentWithMultipleAttempts + + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertTrue(viewModel.uiState.value.toolsBottomSheetUiState.showAttemptSelector) + } + + @Test + fun `Test attempt selector is hidden for single attempt assignment`() = runTest { + val assignmentWithSingleAttempt = testAssignment.copy(allowedAttempts = 1L) + coEvery { repository.getAssignment(any(), any(), any()) } returns assignmentWithSingleAttempt + + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + assertFalse(viewModel.uiState.value.toolsBottomSheetUiState.showAttemptSelector) + } + + @Test + fun `Test opening assignment tools updates UI state`() = runTest { + val savedStateHandle = SavedStateHandle(mapOf( + ModuleItemContent.Assignment.ASSIGNMENT_ID to assignmentId, + Const.COURSE_ID to courseId + )) + + val viewModel = getViewModel(savedStateHandle) + + viewModel.openAssignmentTools() + + assertTrue(viewModel.uiState.value.toolsBottomSheetUiState.show) + } + + private fun getViewModel(savedStateHandle: SavedStateHandle): AssignmentDetailsViewModel { + return AssignmentDetailsViewModel( + context, + repository, + htmlContentFormatter, + aiAssistContextProvider, + savedStateHandle + ) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsRepositoryTest.kt new file mode 100644 index 0000000000..bbb8147381 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsRepositoryTest.kt @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.moduleitemsequence.content.assignment.comments + +import com.instructure.canvasapi2.apis.SubmissionAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.managers.graphql.horizon.Comment +import com.instructure.canvasapi2.managers.graphql.horizon.CommentsData +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCommentsManager +import com.instructure.canvasapi2.models.Submission +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +class CommentsRepositoryTest { + private val getCommentsManager: HorizonGetCommentsManager = mockk(relaxed = true) + private val submissionApi: SubmissionAPI.SubmissionInterface = mockk(relaxed = true) + + private lateinit var repository: CommentsRepository + + private val testCommentsData = CommentsData( + comments = listOf( + Comment( + authorId = 100L, + authorName = "Student", + createdAt = Date(), + commentText = "Test comment", + read = true, + attachments = emptyList() + ) + ), + endCursor = "cursor-end", + startCursor = "cursor-start", + hasNextPage = true, + hasPreviousPage = false + ) + + private val testSubmission = Submission( + id = 1L, + attempt = 1, + userId = 100L + ) + + @Before + fun setup() { + repository = CommentsRepository(getCommentsManager, submissionApi) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `getComments returns comments data successfully`() = runTest { + coEvery { getCommentsManager.getComments(any(), any(), any(), any(), any(), any(), any()) } returns testCommentsData + + val result = repository.getComments( + assignmentId = 1L, + userId = 100L, + attempt = 1, + forceNetwork = false + ) + + assertEquals(1, result.comments.size) + assertEquals("Test comment", result.comments.first().commentText) + assertEquals("cursor-end", result.endCursor) + assertTrue(result.hasNextPage) + coVerify { getCommentsManager.getComments(1L, 100L, 1, false, false, null, null) } + } + + @Test + fun `getComments with pagination parameters`() = runTest { + coEvery { getCommentsManager.getComments(any(), any(), any(), any(), any(), any(), any()) } returns testCommentsData + + repository.getComments( + assignmentId = 1L, + userId = 100L, + attempt = 1, + forceNetwork = true, + startCursor = "start", + endCursor = "end", + nextPage = true + ) + + coVerify { getCommentsManager.getComments(1L, 100L, 1, true, true,"end", "start") } + } + + @Test + fun `getComments with forceNetwork true`() = runTest { + coEvery { getCommentsManager.getComments(any(), any(), any(), any(), any(), any(), any()) } returns testCommentsData + + repository.getComments( + assignmentId = 1L, + userId = 100L, + attempt = 1, + forceNetwork = true + ) + + coVerify { getCommentsManager.getComments(any(), any(), any(), any(), true, any(), any()) } + } + + @Test + fun `getComments for next page`() = runTest { + coEvery { getCommentsManager.getComments(any(), any(), any(), any(), any(), any(), any()) } returns testCommentsData + + repository.getComments( + assignmentId = 1L, + userId = 100L, + attempt = 1, + forceNetwork = false, + endCursor = "cursor-end", + nextPage = true + ) + + coVerify { getCommentsManager.getComments(any(), any(), any(), true, false, "cursor-end", null) } + } + + @Test + fun `getComments for previous page`() = runTest { + coEvery { getCommentsManager.getComments(any(), any(), any(), any(), any(), any(), any()) } returns testCommentsData + + repository.getComments( + assignmentId = 1L, + userId = 100L, + attempt = 1, + forceNetwork = false, + startCursor = "cursor-start", + nextPage = false + ) + + coVerify { getCommentsManager.getComments(any(), any(), any(), false, false, null, "cursor-start") } + } + + @Test + fun `postComment returns success result`() = runTest { + coEvery { submissionApi.postSubmissionComment(any(), any(), any(), any(), any(), any(), any(), any()) } returns DataResult.Success(testSubmission) + + val result = repository.postComment( + courseId = 1L, + assignmentId = 10L, + userId = 100L, + attempt = 1, + commentText = "My comment" + ) + + assertTrue(result is DataResult.Success) + coVerify { + submissionApi.postSubmissionComment( + courseId = 1L, + assignmentId = 10L, + userId = 100L, + comment = "My comment", + attemptId = 1L, + isGroupComment = false, + attachments = listOf(), + restParams = any() + ) + } + } + + @Test + fun `postComment with different attempt`() = runTest { + coEvery { submissionApi.postSubmissionComment(any(), any(), any(), any(), any(), any(), any(), any()) } returns DataResult.Success(testSubmission) + + repository.postComment( + courseId = 1L, + assignmentId = 10L, + userId = 100L, + attempt = 5, + commentText = "Comment on attempt 5" + ) + + coVerify { + submissionApi.postSubmissionComment( + courseId = any(), + assignmentId = any(), + userId = any(), + comment = any(), + attemptId = 5L, + isGroupComment = any(), + attachments = any(), + restParams = any(), + ) + } + } + + @Test + fun `postComment returns failure result`() = runTest { + coEvery { submissionApi.postSubmissionComment(any(), any(), any(), any(), any(), any(), any(), any()) } returns DataResult.Fail() + + val result = repository.postComment( + courseId = 1L, + assignmentId = 10L, + userId = 100L, + attempt = 1, + commentText = "My comment" + ) + + assertTrue(result is DataResult.Fail) + } + + @Test + fun `getComments with attempt 0`() = runTest { + coEvery { getCommentsManager.getComments(any(), any(), any(), any(), any(), any(), any()) } returns testCommentsData + + repository.getComments( + assignmentId = 1L, + userId = 100L, + attempt = 0, + forceNetwork = false + ) + + coVerify { getCommentsManager.getComments(1L, 100L, 0, false, false, null, null) } + } + + @Test + fun `postComment always sets isGroupComment to false`() = runTest { + coEvery { submissionApi.postSubmissionComment(any(), any(), any(), any(), any(), any(), any(), any()) } returns DataResult.Success(testSubmission) + + repository.postComment( + courseId = 1L, + assignmentId = 10L, + userId = 100L, + attempt = 1, + commentText = "Comment" + ) + + coVerify { + submissionApi.postSubmissionComment( + courseId = any(), + assignmentId = any(), + userId = any(), + comment = any(), + attemptId = any(), + isGroupComment = false, + attachments = any(), + restParams = any(), + ) + } + } + + @Test + fun `postComment always sends empty attachments list`() = runTest { + coEvery { submissionApi.postSubmissionComment(any(), any(), any(), any(), any(), any(), any(), any()) } returns DataResult.Success(testSubmission) + + repository.postComment( + courseId = 1L, + assignmentId = 10L, + userId = 100L, + attempt = 1, + commentText = "Comment" + ) + + coVerify { + submissionApi.postSubmissionComment( + courseId = any(), + assignmentId = any(), + userId = any(), + comment = any(), + attemptId = 1L, + isGroupComment = any(), + attachments = emptyList(), + restParams = any(), + ) + } + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsViewModelTest.kt new file mode 100644 index 0000000000..6593695e79 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/assignment/comments/CommentsViewModelTest.kt @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.moduleitemsequence.content.assignment.comments + +import android.content.Context +import android.text.format.DateFormat +import androidx.compose.ui.text.input.TextFieldValue +import androidx.work.WorkManager +import androidx.work.WorkRequest +import com.instructure.canvasapi2.managers.graphql.horizon.Comment +import com.instructure.canvasapi2.managers.graphql.horizon.CommentsData +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.horizon.R +import com.instructure.pandautils.room.appdatabase.daos.FileDownloadProgressDao +import com.instructure.pandautils.room.appdatabase.entities.FileDownloadProgressEntity +import com.instructure.pandautils.room.appdatabase.entities.FileDownloadProgressState +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class CommentsViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: CommentsRepository = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val workManager: WorkManager = mockk(relaxed = true) + private val fileDownloadProgressDao: FileDownloadProgressDao = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val assignmentId = 1L + private val courseId = 100L + private val attempt = 1 + private val userId = 123L + + private val testCommentsData = CommentsData( + comments = listOf( + Comment( + authorId = userId, + authorName = "Test User", + createdAt = Date(), + commentText = "Test comment", + read = true, + attachments = emptyList() + ) + ), + endCursor = "cursor1", + startCursor = null, + hasNextPage = true, + hasPreviousPage = false + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + mockkStatic(DateFormat::class) + every { DateFormat.is24HourFormat(any()) } returns false + every { DateFormat.getBestDateTimePattern(any(), any()) } returns "" + every { apiPrefs.user } returns User(id = userId, name = "Test User") + coEvery { repository.getComments(any(), any(), any(), any(), any(), any()) } returns testCommentsData + coEvery { repository.postComment(any(), any(), any(), any(), any()) } returns DataResult.Success(mockk(relaxed = true)) + coEvery { fileDownloadProgressDao.findByWorkerIdFlow(any()) } returns flowOf(null) + coEvery { fileDownloadProgressDao.deleteByWorkerId(any()) } returns Unit + every { workManager.enqueue(any()) } returns mockk { + every { result } returns mockk() + } + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test init with attempt loads comments`() = runTest { + val viewModel = getViewModel() + + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + assertFalse(viewModel.uiState.value.loading) + assertEquals(1, viewModel.uiState.value.comments.size) + assertEquals("Test comment", viewModel.uiState.value.comments.first().commentText) + coVerify { repository.getComments(assignmentId, userId, attempt, false, null, null) } + } + + @Test + fun `Test comments are mapped with user info`() = runTest { + val viewModel = getViewModel() + + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + val comment = viewModel.uiState.value.comments.first() + assertEquals("Test User", comment.title) + assertTrue(comment.fromCurrentUser) + assertTrue(comment.read) + } + + @Test + fun `Test paging controls visibility`() = runTest { + val viewModel = getViewModel() + + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + assertTrue(viewModel.uiState.value.showPagingControls) + assertTrue(viewModel.uiState.value.nextPageEnabled) + assertFalse(viewModel.uiState.value.previousPageEnabled) + } + + @Test + fun `Test load next page`() = runTest { + val nextPageData = testCommentsData.copy( + comments = listOf( + Comment( + authorId = userId, + authorName = "Test User", + createdAt = Date(), + commentText = "Next page comment", + read = true, + attachments = emptyList() + ) + ), + hasNextPage = false, + hasPreviousPage = true + ) + coEvery { repository.getComments(any(), any(), any(), any(), any(), any(), any()) } returns testCommentsData andThen nextPageData + + val viewModel = getViewModel() + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + viewModel.uiState.value.onNextPageClicked() + + assertEquals("Next page comment", viewModel.uiState.value.comments.first().commentText) + coVerify { repository.getComments(assignmentId, userId, attempt, any(), null, "cursor1", any()) } + } + + @Test + fun `Test load previous page`() = runTest { + val firstPageData = testCommentsData.copy(hasPreviousPage = true) + coEvery { repository.getComments(any(), any(), any(), any(), any(), any()) } returns firstPageData andThen testCommentsData + + val viewModel = getViewModel() + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + viewModel.uiState.value.onPreviousPageClicked() + + coVerify { repository.getComments(assignmentId, userId, attempt, false, null, null) } + } + + @Test + fun `Test comment text change updates state`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onCommentChanged(TextFieldValue("New comment")) + + assertEquals("New comment", viewModel.uiState.value.comment.text) + } + + @Test + fun `Test post comment with valid text`() { + val viewModel = getViewModel() + viewModel.initWithAttempt(assignmentId, attempt, courseId) + viewModel.uiState.value.onCommentChanged(TextFieldValue("New comment")) + + viewModel.uiState.value.onPostClicked() + + coVerify { repository.postComment(courseId, assignmentId, userId, attempt, "New comment") } + assertEquals("", viewModel.uiState.value.comment.text) + assertFalse(viewModel.uiState.value.postingComment) + } + + @Test + fun `Test post comment with blank text does nothing`() = runTest { + val viewModel = getViewModel() + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + viewModel.uiState.value.onPostClicked() + + coVerify(exactly = 0) { repository.postComment(any(), any(), any(), any(), any()) } + } + + @Test + fun `Test post comment error shows message`() = runTest { + coEvery { repository.postComment(any(), any(), any(), any(), any()) } throws Exception("Post error") + + val viewModel = getViewModel() + viewModel.initWithAttempt(assignmentId, attempt, courseId) + viewModel.uiState.value.onCommentChanged(TextFieldValue("New comment")) + + viewModel.uiState.value.onPostClicked() + + assertNotNull(viewModel.uiState.value.errorMessage) + assertFalse(viewModel.uiState.value.postingComment) + } + + @Test + fun `Test error dismissed clears message`() = runTest { + coEvery { repository.getComments(any(), any(), any(), any(), any(), any()) } throws Exception("Load error") + + val viewModel = getViewModel() + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + viewModel.uiState.value.onErrorDismissed() + + assertNull(viewModel.uiState.value.errorMessage) + } + + @Test + fun `Test file opened clears file path`() = runTest { + val completedEntity = FileDownloadProgressEntity( + workerId = "worker-id", + progressState = FileDownloadProgressState.COMPLETED, + progress = 100, + filePath = "/path/to/file", + fileName = "fileName" + ) + coEvery { fileDownloadProgressDao.findByWorkerIdFlow(any()) } returns flowOf(completedEntity) + + val viewModel = getViewModel() + + viewModel.uiState.value.onFileOpened() + + assertNull(viewModel.uiState.value.filePathToOpen) + assertNull(viewModel.uiState.value.mimeTypeToOpen) + } + + @Test + fun `Test load comments error shows message`() = runTest { + coEvery { repository.getComments(any(), any(), any(), any(), any(), any()) } throws Exception("Load error") + + val viewModel = getViewModel() + + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + assertFalse(viewModel.uiState.value.loading) + assertNotNull(viewModel.uiState.value.errorMessage) + } + + @Test + fun `Test attempt 0 shows no subtitle`() = runTest { + val viewModel = getViewModel() + + viewModel.initWithAttempt(assignmentId, 0, courseId) + + val comment = viewModel.uiState.value.comments.first() + assertEquals("", comment.subtitle) + } + + @Test + fun `Test non-zero attempt shows subtitle`() = runTest { + every { context.getString(R.string.commentsBottomSheet_attempt, any()) } returns "Attempt $attempt" + val viewModel = getViewModel() + + viewModel.initWithAttempt(assignmentId, 2, courseId) + + val comment = viewModel.uiState.value.comments.first() + assertTrue(comment.subtitle.isNotEmpty()) + } + + @Test + fun `Test comment from different user`() = runTest { + val otherUserComment = testCommentsData.copy( + comments = listOf( + Comment( + authorId = 999L, + authorName = "Other User", + createdAt = Date(), + commentText = "Other comment", + read = false, + attachments = emptyList() + ) + ) + ) + coEvery { repository.getComments(any(), any(), any(), any(), any(), any(), any()) } returns otherUserComment + + val viewModel = getViewModel() + viewModel.initWithAttempt(assignmentId, attempt, courseId) + + val comment = viewModel.uiState.value.comments.first() + assertFalse(comment.fromCurrentUser) + assertFalse(comment.read) + } + + private fun getViewModel(): CommentsViewModel { + return CommentsViewModel( + context, + repository, + apiPrefs, + workManager, + fileDownloadProgressDao + ) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsRepositoryTest.kt new file mode 100644 index 0000000000..2c7e74f30d --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsRepositoryTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.moduleitemsequence.content.page + +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.RedwoodApiManager +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.redwood.QueryNotesQuery +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +class PageDetailsRepositoryTest { + private val pageApi: PageAPI.PagesInterface = mockk(relaxed = true) + private val oAuthInterface: OAuthAPI.OAuthInterface = mockk(relaxed = true) + private val redwoodApi: RedwoodApiManager = mockk(relaxed = true) + + private lateinit var repository: PageDetailsRepository + + private val testPage = Page( + id = 1L, + url = "test-page", + title = "Test Page", + body = "

Page content

" + ) + + private val testNotes = QueryNotesQuery.Notes( + pageInfo = QueryNotesQuery.PageInfo( + hasNextPage = false, + hasPreviousPage = false, + startCursor = null, + endCursor = null + ), + edges = listOf( + QueryNotesQuery.Edge( + cursor = "", + node = QueryNotesQuery.Node( + id = "1", + objectId = "1", + objectType = "Page", + userText = "comment 1", + rootAccountUuid = "1", + userId = "1", + courseId = "1", + reaction = listOf("Important"), + highlightData = "", + createdAt = Date(), + updatedAt = Date(), + ) + ), + QueryNotesQuery.Edge( + cursor = "", + node = QueryNotesQuery.Node( + id = "2", + objectId = "1", + objectType = "Page", + userText = "comment 2", + rootAccountUuid = "1", + userId = "1", + courseId = "1", + reaction = listOf("Important"), + highlightData = "", + createdAt = Date(), + updatedAt = Date(), + ) + ) + ) + ) + + @Before + fun setup() { + repository = PageDetailsRepository(pageApi, oAuthInterface, redwoodApi) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `getPageDetails returns page successfully`() = runTest { + coEvery { pageApi.getDetailedPage(any(), any(), any(), any()) } returns DataResult.Success(testPage) + + val result = repository.getPageDetails(courseId = 1L, pageId = "test-page", forceNetwork = false) + + assertEquals("Test Page", result.title) + assertEquals("

Page content

", result.body) + coVerify { pageApi.getDetailedPage("courses", 1L, "test-page", any()) } + } + + @Test + fun `getPageDetails with forceNetwork true`() = runTest { + coEvery { pageApi.getDetailedPage(any(), any(), any(), any()) } returns DataResult.Success(testPage) + + repository.getPageDetails(courseId = 1L, pageId = "test-page", forceNetwork = true) + + coVerify { pageApi.getDetailedPage(any(), any(), any(), match { it.isForceReadFromNetwork }) } + } + + @Test + fun `getPageDetails with forceNetwork false`() = runTest { + coEvery { pageApi.getDetailedPage(any(), any(), any(), any()) } returns DataResult.Success(testPage) + + repository.getPageDetails(courseId = 1L, pageId = "test-page", forceNetwork = false) + + coVerify { pageApi.getDetailedPage(any(), any(), any(), match { !it.isForceReadFromNetwork }) } + } + + @Test + fun `authenticateUrl returns authenticated URL`() = runTest { + val session = AuthenticatedSession(sessionUrl = "https://authenticated.url") + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Success(session) + + val result = repository.authenticateUrl("https://example.com/page") + + assertEquals("https://authenticated.url", result) + coVerify { oAuthInterface.getAuthenticatedSession("https://example.com/page", any()) } + } + + @Test + fun `authenticateUrl returns original URL on failure`() = runTest { + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Fail() + + val result = repository.authenticateUrl("https://example.com/page") + + assertEquals("https://example.com/page", result) + } + + @Test + fun `authenticateUrl returns original URL when session URL is null`() = runTest { + val session = AuthenticatedSession(sessionUrl = "https://example.com/page/authenticated") + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Success(session) + + val result = repository.authenticateUrl("https://example.com/page") + + assertEquals("https://example.com/page/authenticated", result) + } + + @Test + fun `getNotes returns notes list`() = runTest { + coEvery { redwoodApi.getNotes(any(), any(), any()) } returns testNotes + + val result = repository.getNotes(courseId = 1L, pageId = 100L) + + assertEquals(2, result.size) + assertEquals("comment 1", result.first().userText) + coVerify { redwoodApi.getNotes(any(), null, null) } + } + + @Test + fun `getNotes with different page ID`() = runTest { + coEvery { redwoodApi.getNotes(any(), any(), any()) } returns testNotes + + repository.getNotes(courseId = 5L, pageId = 200L) + + coVerify { redwoodApi.getNotes(any(), null, null) } + } + + @Test + fun `getNotes returns empty list`() = runTest { + coEvery { redwoodApi.getNotes(any(), any(), any()) } returns QueryNotesQuery.Notes( + pageInfo = QueryNotesQuery.PageInfo( + hasNextPage = false, + hasPreviousPage = false, + startCursor = null, + endCursor = null + ), + edges = emptyList() + ) + + val result = repository.getNotes(courseId = 1L, pageId = 100L) + + assertEquals(0, result.size) + } + + @Test + fun `authenticateUrl always uses forceNetwork`() = runTest { + val session = AuthenticatedSession(sessionUrl = "https://authenticated.url") + coEvery { oAuthInterface.getAuthenticatedSession(any(), any()) } returns DataResult.Success(session) + + repository.authenticateUrl("https://example.com") + + coVerify { oAuthInterface.getAuthenticatedSession(any(), match { it.isForceReadFromNetwork }) } + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsViewModelTest.kt new file mode 100644 index 0000000000..2c697376d7 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsViewModelTest.kt @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.moduleitemsequence.content.page + +import androidx.lifecycle.SavedStateHandle +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataRange +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataTextPosition +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteObjectType +import com.instructure.canvasapi2.models.Page +import com.instructure.horizon.features.moduleitemsequence.ModuleItemContent +import com.instructure.horizon.features.notebook.addedit.add.AddNoteRepository +import com.instructure.horizon.features.notebook.common.model.Note +import com.instructure.horizon.features.notebook.common.model.NotebookType +import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.HtmlContentFormatter +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class PageDetailsViewModelTest { + private val repository: PageDetailsRepository = mockk(relaxed = true) + private val htmlContentFormatter: HtmlContentFormatter = mockk(relaxed = true) + private val addNoteRepository: AddNoteRepository = mockk(relaxed = true) + private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val courseId = 1L + private val pageUrl = "test-page" + private val testPage = Page( + id = 100L, + url = pageUrl, + title = "Test Page", + body = "

Test content

" + ) + + private val testNotes = listOf( + Note( + id = "1", + objectId = "1", + objectType = NoteObjectType.PAGE, + userText = "comment 1", + highlightedText = NoteHighlightedData( + selectedText = "highlighted text 1", + range = NoteHighlightedDataRange(1, 5, "start", "end"), + textPosition = NoteHighlightedDataTextPosition(1, 5) + ), + type = NotebookType.Important, + updatedAt = Date(), + courseId = 1, + ), + Note( + id = "2", + objectId = "1", + objectType = NoteObjectType.PAGE, + userText = "comment 2", + highlightedText = NoteHighlightedData( + selectedText = "highlighted text 2", + range = NoteHighlightedDataRange(10, 15, "start", "end"), + textPosition = NoteHighlightedDataTextPosition(10, 15) + ), + type = NotebookType.Confusing, + updatedAt = Date(), + courseId = 1, + ) + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + every { savedStateHandle.get(Const.COURSE_ID) } returns courseId + every { savedStateHandle.get(ModuleItemContent.Page.PAGE_URL) } returns pageUrl + coEvery { repository.getPageDetails(any(), any()) } returns testPage + coEvery { repository.getNotes(any(), any()) } returns testNotes + coEvery { repository.authenticateUrl(any()) } returns "https://authenticated.url" + coEvery { htmlContentFormatter.formatHtmlWithIframes(any(), any()) } answers { firstArg() } + coEvery { addNoteRepository.addNote(any(), any(), any(), any(), any(), any()) } returns Unit + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel loads page details`() = runTest { + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + assertEquals("

Test content

", viewModel.uiState.value.pageHtmlContent) + assertEquals(100L, viewModel.uiState.value.pageId) + assertEquals(pageUrl, viewModel.uiState.value.pageUrl) + coVerify { repository.getPageDetails(courseId, pageUrl) } + } + + @Test + fun `Test HTML content is formatted`() = runTest { + coEvery { htmlContentFormatter.formatHtmlWithIframes(any(), any()) } returns "formatted html" + + val viewModel = getViewModel() + + assertEquals("formatted html", viewModel.uiState.value.pageHtmlContent) + coVerify { htmlContentFormatter.formatHtmlWithIframes("

Test content

", courseId) } + } + + @Test + fun `Test notes are loaded`() = runTest { + val viewModel = getViewModel() + + assertEquals(2, viewModel.uiState.value.notes.size) + assertEquals("comment 1", viewModel.uiState.value.notes.first().userText) + coVerify { repository.getNotes(courseId, 100L) } + } + + @Test + fun `Test notes loading failure does not fail page load`() = runTest { + coEvery { repository.getNotes(any(), any()) } throws Exception("Notes error") + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + assertTrue(viewModel.uiState.value.notes.isEmpty()) + assertNotNull(viewModel.uiState.value.pageHtmlContent) + } + + @Test + fun `Test LTI button pressed authenticates URL`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.ltiButtonPressed?.invoke("https://lti.url") + + assertEquals("https://authenticated.url", viewModel.uiState.value.urlToOpen) + coVerify { repository.authenticateUrl("https://lti.url") } + } + + @Test + fun `Test LTI authentication failure returns original URL`() = runTest { + coEvery { repository.authenticateUrl(any()) } throws Exception("Auth error") + + val viewModel = getViewModel() + + viewModel.uiState.value.ltiButtonPressed?.invoke("https://lti.url") + + assertEquals("https://lti.url", viewModel.uiState.value.urlToOpen) + } + + @Test + fun `Test URL opened clears URL to open`() = runTest { + val viewModel = getViewModel() + viewModel.uiState.value.ltiButtonPressed?.invoke("https://lti.url") + + viewModel.uiState.value.onUrlOpened() + + assertNull(viewModel.uiState.value.urlToOpen) + } + + @Test + fun `Test add note creates note and refreshes`() = runTest { + val viewModel = getViewModel() + val highlightedData = NoteHighlightedData( + selectedText = "highlighted text", + range = NoteHighlightedDataRange(1, 5, "start", "end"), + textPosition = NoteHighlightedDataTextPosition(1, 5) + ) + + viewModel.uiState.value.addNote(highlightedData, "Important") + + coVerify { addNoteRepository.addNote( + courseId = courseId.toString(), + objectId = testPage.id.toString(), + objectType = "Page", + highlightedData = highlightedData, + userComment = "", + type = NotebookType.Important + ) } + + coVerify(atLeast = 2) { repository.getNotes(courseId, 100L) } + } + + @Test + fun `Test refresh notes updates state`() = runTest { + val updatedNotes = testNotes + testNotes.last().copy(userText = "New note") + coEvery { repository.getNotes(any(), any()) } returns testNotes andThen updatedNotes + + val viewModel = getViewModel() + assertEquals(2, viewModel.uiState.value.notes.size) + + viewModel.refreshNotes() + + assertEquals(3, viewModel.uiState.value.notes.size) + assertEquals("New note", viewModel.uiState.value.notes.last().userText) + } + + @Test + fun `Test refresh notes handles error`() = runTest { + val viewModel = getViewModel() + coEvery { repository.getNotes(any(), any()) } throws Exception("Error") + + // Should not crash + viewModel.refreshNotes() + } + + @Test + fun `Test page load error sets error state`() = runTest { + coEvery { repository.getPageDetails(any(), any()) } throws Exception("Error") + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.loadingState.isLoading) + assertTrue(viewModel.uiState.value.loadingState.isError) + } + + @Test + fun `Test course ID is set in UI state`() = runTest { + val viewModel = getViewModel() + + assertEquals(courseId, viewModel.uiState.value.courseId) + } + + private fun getViewModel(): PageDetailsViewModel { + return PageDetailsViewModel( + repository, + htmlContentFormatter, + addNoteRepository, + savedStateHandle + ) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/NotebookRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/NotebookRepositoryTest.kt new file mode 100644 index 0000000000..9f53185ef9 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/NotebookRepositoryTest.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.notebook + +import com.apollographql.apollo.api.Optional +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.RedwoodApiManager +import com.instructure.horizon.features.notebook.common.model.NotebookType +import com.instructure.redwood.QueryNotesQuery +import com.instructure.redwood.type.LearningObjectFilter +import com.instructure.redwood.type.NoteFilterInput +import com.instructure.redwood.type.OrderDirection +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class NotebookRepositoryTest { + private val redwoodApiManager: RedwoodApiManager = mockk(relaxed = true) + + @Test + fun `Test successful notes retrieval with filter`() = runTest { + val mockNotes = QueryNotesQuery.Notes( + edges = listOf(), + pageInfo = QueryNotesQuery.PageInfo(hasNextPage = false, hasPreviousPage = false, endCursor = null, startCursor = null) + ) + coEvery { redwoodApiManager.getNotes(any(), any(), any(), any(), any(), any()) } returns mockNotes + + val result = getRepository().getNotes( + filterType = NotebookType.Important, + courseId = 1L, + orderDirection = OrderDirection.descending + ) + + assertNotNull(result) + assertEquals(mockNotes, result) + } + + @Test + fun `Test notes retrieval with pagination after cursor`() = runTest { + val mockNotes = QueryNotesQuery.Notes( + edges = listOf(), + pageInfo = QueryNotesQuery.PageInfo(hasNextPage = true, hasPreviousPage = false, endCursor = "cursor123", startCursor = null) + ) + coEvery { redwoodApiManager.getNotes(any(), any(), any(), any(), any(), any()) } returns mockNotes + + getRepository().getNotes(after = "cursor123") + + coVerify { redwoodApiManager.getNotes(filter = any(), firstN = 10, lastN = any(), after = "cursor123", before = any(), orderBy = any()) } + } + + @Test + fun `Test notes retrieval with pagination before cursor`() = runTest { + val mockNotes = QueryNotesQuery.Notes( + edges = listOf(), + pageInfo = QueryNotesQuery.PageInfo(hasNextPage = false, hasPreviousPage = true, endCursor = null, startCursor = "cursor456") + ) + coEvery { redwoodApiManager.getNotes(any(), any(), any(), any(), any(), any()) } returns mockNotes + + getRepository().getNotes(before = "cursor456") + + coVerify { redwoodApiManager.getNotes(filter = any(), firstN = any(), lastN = 10, after = any(), before = "cursor456", orderBy = any()) } + } + + @Test + fun `Test notes retrieval with course filter`() = runTest { + val courseId = 123L + val mockNotes = QueryNotesQuery.Notes( + edges = listOf(), + pageInfo = QueryNotesQuery.PageInfo(hasNextPage = false, hasPreviousPage = false, endCursor = null, startCursor = null) + ) + coEvery { redwoodApiManager.getNotes(any(), any(), any(), any(), any(), any()) } returns mockNotes + + getRepository().getNotes(courseId = courseId) + + coVerify { redwoodApiManager.getNotes(NoteFilterInput( + courseId = Optional.present(courseId.toString()), + ), any(), any(), any(), any(), any()) } + } + + @Test + fun `Test notes retrieval with learning object filter`() = runTest { + val objectTypeAndId = Pair("Assignment", "456") + val mockNotes = QueryNotesQuery.Notes( + edges = listOf(), + pageInfo = QueryNotesQuery.PageInfo(hasNextPage = false, hasPreviousPage = false, endCursor = null, startCursor = null) + ) + coEvery { redwoodApiManager.getNotes(any(), any(), any(), any(), any(), any()) } returns mockNotes + + getRepository().getNotes(objectTypeAndId = objectTypeAndId) + + coVerify { redwoodApiManager.getNotes(NoteFilterInput( + learningObject = Optional.present(LearningObjectFilter( + type = objectTypeAndId.first, + id = objectTypeAndId.second + )) + ), any(), any(), any(), any(), any()) } + } + + @Test + fun `Test notes retrieval with custom item count`() = runTest { + val mockNotes = QueryNotesQuery.Notes( + edges = listOf(), + pageInfo = QueryNotesQuery.PageInfo(hasNextPage = false, hasPreviousPage = false, endCursor = null, startCursor = null) + ) + coEvery { redwoodApiManager.getNotes(any(), any(), any(), any(), any(), any()) } returns mockNotes + + getRepository().getNotes(itemCount = 25) + + coVerify { redwoodApiManager.getNotes(filter = any(), firstN = 25, lastN = any(), after = any(), before = any(), orderBy = any()) } + } + + @Test + fun `Test notes retrieval with reaction filter`() = runTest { + val mockNotes = QueryNotesQuery.Notes( + edges = listOf(), + pageInfo = QueryNotesQuery.PageInfo(hasNextPage = false, hasPreviousPage = false, endCursor = null, startCursor = null) + ) + coEvery { redwoodApiManager.getNotes(any(), any(), any(), any(), any(), any()) } returns mockNotes + + getRepository().getNotes(filterType = NotebookType.Confusing) + + coVerify { redwoodApiManager.getNotes(NoteFilterInput( + reactions = Optional.present(listOf("Confusing")) + ), any(), any(), any(), any(), any()) } + } + + private fun getRepository(): NotebookRepository { + return NotebookRepository(redwoodApiManager) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/NotebookViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/NotebookViewModelTest.kt new file mode 100644 index 0000000000..2a2ab3ced0 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/NotebookViewModelTest.kt @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.notebook + +import com.instructure.horizon.features.notebook.common.model.NotebookType +import com.instructure.redwood.QueryNotesQuery +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class NotebookViewModelTest { + private val repository: NotebookRepository = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val testNotes = QueryNotesQuery.Notes( + edges = listOf( + QueryNotesQuery.Edge( + cursor = "cursor1", + node = QueryNotesQuery.Node( + id = "note1", + userText = "Test note 1", + createdAt = Date(), + updatedAt = Date(), + rootAccountUuid = "", + userId = "1", + courseId = "1", + objectId = "1", + objectType = "Assignment", + reaction = listOf(""), + highlightData = "test" + ) + ) + ), + pageInfo = QueryNotesQuery.PageInfo( + hasNextPage = true, + hasPreviousPage = false, + endCursor = "endCursor1", + startCursor = "startCursor1" + ) + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + coEvery { repository.getNotes(any(), any(), any(), any(), any(), any(), any()) } returns testNotes + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test data loads successfully on init`() { + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.isLoading) + coVerify { repository.getNotes(any(), any(), any(), any(), any(), any(), any()) } + } + + @Test + fun `Test notes are loaded`() { + val viewModel = getViewModel() + + assertTrue(viewModel.uiState.value.notes.isNotEmpty()) + } + + @Test + fun `Test failed data load sets error state`() = runTest { + coEvery { repository.getNotes(any(), any(), any(), any(), any(), any(), any()) } throws Exception("Network error") + + val viewModel = getViewModel() + + assertFalse(viewModel.uiState.value.isLoading) + assertTrue(viewModel.uiState.value.notes.isEmpty()) + } + + @Test + fun `Test pagination info is updated`() { + val viewModel = getViewModel() + + assertTrue(viewModel.uiState.value.hasNextPage) + assertFalse(viewModel.uiState.value.hasPreviousPage) + } + + @Test + fun `Test filter selection updates state and reloads data`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFilterSelected(NotebookType.Important) + + assertEquals(NotebookType.Important, viewModel.uiState.value.selectedFilter) + coVerify(atLeast = 2) { repository.getNotes(any(), any(), any(), any(), any(), any(), any()) } + } + + @Test + fun `Test filter selection with null clears filter`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.onFilterSelected(null) + + assertEquals(null, viewModel.uiState.value.selectedFilter) + } + + @Test + fun `Test load next page uses end cursor`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.loadNextPage() + + coVerify { repository.getNotes(after = "endCursor1", before = null, any(), any(), any(), any(), any()) } + } + + @Test + fun `Test load previous page uses start cursor`() = runTest { + val notesWithPrevious = testNotes.copy( + pageInfo = testNotes.pageInfo.copy(hasPreviousPage = true) + ) + coEvery { repository.getNotes(any(), any(), any(), any(), any(), any(), any()) } returns notesWithPrevious + + val viewModel = getViewModel() + + viewModel.uiState.value.loadPreviousPage() + + coVerify { repository.getNotes(after = null, before = "startCursor1", any(), any(), any(), any(), any()) } + } + + @Test + fun `Test update course id reloads data`() = runTest { + val viewModel = getViewModel() + + viewModel.updateCourseId(123L) + viewModel.updateCourseId(1234L) + viewModel.updateCourseId(123L) + + coVerify(exactly = 2) { repository.getNotes(any(), any(), any(), any(), 123L, any(), any()) } + } + + @Test + fun `Test update content with course id hides top bar`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateContent(123L, null) + + assertFalse(viewModel.uiState.value.showTopBar) + assertTrue(viewModel.uiState.value.showFilters) + } + + @Test + fun `Test update content with object type hides filters`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateContent(123L, Pair("Assignment", "456")) + + assertFalse(viewModel.uiState.value.showTopBar) + assertFalse(viewModel.uiState.value.showFilters) + } + + @Test + fun `Test update content without course id shows top bar and filters`() = runTest { + val viewModel = getViewModel() + + viewModel.uiState.value.updateContent(null, null) + + assertTrue(viewModel.uiState.value.showTopBar) + assertTrue(viewModel.uiState.value.showFilters) + } + + private fun getViewModel(): NotebookViewModel { + return NotebookViewModel(repository) + } +} diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/notification/NotificationRepositoryTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/notification/NotificationRepositoryTest.kt new file mode 100644 index 0000000000..695c106e9e --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/notification/NotificationRepositoryTest.kt @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.notification + +import com.instructure.canvasapi2.apis.AccountNotificationAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.StreamAPI +import com.instructure.canvasapi2.managers.graphql.horizon.CourseWithProgress +import com.instructure.canvasapi2.managers.graphql.horizon.HorizonGetCoursesManager +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.StreamItem +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class NotificationRepositoryTest { + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val streamApi: StreamAPI.StreamInterface = mockk(relaxed = true) + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val accountNotificationApi: AccountNotificationAPI.AccountNotificationInterface = mockk(relaxed = true) + private val getCoursesManager: HorizonGetCoursesManager = mockk(relaxed = true) + + private val userId = 1L + @Before + fun setup() { + every { apiPrefs.user?.id } returns userId + } + + @Test + fun `Test successful Stream API notification items filtering`() = runTest { + val validCourse = CourseWithProgress( + courseId = 1L, + courseName = "Course 1", + courseSyllabus = "", + progress = 5.0 + ) + val notValidCourse = CourseWithProgress( + courseId = 2L, + courseName = "Course 2", + courseSyllabus = "", + progress = 0.0 + ) + val conversationStreamItem = StreamItem( + type = "Conversation", + course_id = validCourse.courseId, + ) + val messageStreamItem = StreamItem( + type = "Message", + course_id = validCourse.courseId, + ) + val discussionStreamItem = StreamItem( + type = "DiscussionTopic", + course_id = validCourse.courseId, + ) + val announcementStreamItem = StreamItem( + type = "Announcement", + course_id = validCourse.courseId, + ) + val notValidAnnouncementStreamItem = StreamItem( + type = "Announcement", + course_id = notValidCourse.courseId, + ) + val dueDateStreamItem = StreamItem( + type = "Message", + notificationCategory = "Due Date", + course_id = validCourse.courseId, + ) + val scoredGradeStreamItem = StreamItem( + type = "Message", + grade = "A", + course_id = validCourse.courseId, + ) + val scoredScoreStreamItem = StreamItem( + type = "Message", + score = 95.0, + course_id = validCourse.courseId, + ) + val gradingPeriodStreamItem = StreamItem( + type = "Message", + notificationCategory = "Grading Policies", + course_id = validCourse.courseId, + ) + val streamItems = listOf(conversationStreamItem, messageStreamItem, discussionStreamItem, announcementStreamItem, notValidAnnouncementStreamItem, dueDateStreamItem, scoredGradeStreamItem, scoredScoreStreamItem, gradingPeriodStreamItem) + + coEvery { streamApi.getUserStream(any()) } returns DataResult.Success(streamItems) + coEvery { getCoursesManager.getCoursesWithProgress(any(), any()) } returns DataResult.Success(listOf(validCourse)) + + val result = getRepository().getNotifications(forceRefresh = true) + + assertEquals(4, result.size) + assertFalse(result.contains(announcementStreamItem)) + assertTrue(result.contains(dueDateStreamItem)) + assertTrue(result.contains(scoredGradeStreamItem)) + assertTrue(result.contains(scoredScoreStreamItem)) + assertTrue(result.contains(gradingPeriodStreamItem)) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed Stream API call`() = runTest { + coEvery { getCoursesManager.getCoursesWithProgress(any(), any()) } returns DataResult.Success(emptyList()) + coEvery { streamApi.getUserStream(any()) } returns DataResult.Fail() + getRepository().getNotifications(true) + } + + @Test + fun `Test successful getCourse by id`() = runTest { + val id = 1L + val course = Course(id = id, name = "Course 1") + coEvery { courseApi.getCourse(id, any()) } returns DataResult.Success(course) + val result = getRepository().getCourse(id) + assertEquals(course, result) + } + + @Test(expected = IllegalStateException::class) + fun `Test failed getCourse by id`() = runTest { + coEvery { courseApi.getCourse(any(), any()) } returns DataResult.Fail() + getRepository().getCourse(1L) + } + + private fun getRepository(): NotificationRepository { + return NotificationRepository(apiPrefs, streamApi, courseApi, getCoursesManager) + } +} \ No newline at end of file diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/notification/NotificationViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/notification/NotificationViewModelTest.kt new file mode 100644 index 0000000000..0a4f11b097 --- /dev/null +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/notification/NotificationViewModelTest.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.features.notification + +import android.content.Context +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.StreamItem +import com.instructure.horizon.R +import com.instructure.horizon.horizonui.molecules.StatusChipColor +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class NotificationViewModelTest { + private val context: Context = mockk(relaxed = true) + private val repository: NotificationRepository = mockk(relaxed = true) + private val testDispatcher = UnconfinedTestDispatcher() + + private val course = Course(1L, "Course 1") + val streamItems = listOf( + StreamItem( + id = 1, + type = "Message", + title = "Scored item", + score = 5.0, + course_id = course.id, + htmlUrl = "deeplinkUrl1", + assignment = Assignment( + id = 1L, + name = "Assignment 1", + htmlUrl = "assignmentUrl1" + ) + ), + StreamItem( + id = 2, + type = "Message", + notificationCategory = "Grading Policies", + title = "Grading period item", + course_id = course.id, + htmlUrl = "deeplinkUrl2" + ), + StreamItem( + id = 3, + type = "Message", + notificationCategory = "Due date", + title = "Due Date", + course_id = course.id, + htmlUrl = "deeplinkUrl3" + ), + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + every { context.getString(R.string.notificationsAnnouncementCategoryLabel) } returns "Announcement" + every { context.getString(R.string.notificationsDueDateCategoryLabel) } returns "Due date" + every { context.getString(R.string.notificationsScoreCategoryLabel) } returns "Score" + every { context.getString(R.string.notificationsScoreChangedCategoryLabel) } returns "Score changed" + every { context.getString(R.string.notificationsScoredItemTitle, "Scored item") } returns "Scored item's score is now available" + coEvery { repository.getCourse(course.id) } returns course + coEvery { repository.getNotifications(any()) } returns streamItems + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + @Test + fun `Test ViewModel mapping to NotificationItems`() { + val viewModel = getViewModel() + val state = viewModel.uiState.value + assertEquals(3, state.notificationItems.size) + assertTrue(state.notificationItems.contains( + NotificationItem( + category = NotificationItemCategory( + "Score changed", + StatusChipColor.Violet + ), + title = "Scored item's score is now available", + courseLabel = null, + date = null, + isRead = false, + deepLink = "assignmentUrl1" + ) + )) + assertTrue(state.notificationItems.contains( + NotificationItem( + category = NotificationItemCategory( + "Score", + StatusChipColor.Violet + ), + title = "Grading period item", + courseLabel = null, + date = null, + isRead = false, + deepLink = "deeplinkUrl2" + ) + )) + assertTrue(state.notificationItems.contains( + NotificationItem( + category = NotificationItemCategory( + "Due date", + StatusChipColor.Honey + ), + title = "Due Date", + courseLabel = null, + date = null, + isRead = false, + deepLink = "deeplinkUrl3" + ) + )) + assertFalse(state.notificationItems.contains( + NotificationItem( + category = NotificationItemCategory( + "Announcement", + StatusChipColor.Sky + ), + title = "Announcement 1", + courseLabel = course.name, + date = null, + isRead = false, + deepLink = "deeplinkUrl4" + ) + )) + } + + private fun getViewModel(): NotificationViewModel { + return NotificationViewModel(context, repository) + } +} \ No newline at end of file diff --git a/libs/interactions/build.gradle b/libs/interactions/build.gradle index f54245df48..05450a79f4 100644 --- a/libs/interactions/build.gradle +++ b/libs/interactions/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.devtools.ksp' apply plugin: 'kotlin-parcelize' static String isTesting() { diff --git a/libs/interactions/src/main/java/com/instructure/interactions/router/RouterParams.kt b/libs/interactions/src/main/java/com/instructure/interactions/router/RouterParams.kt index c00ba6406b..5ded32a939 100644 --- a/libs/interactions/src/main/java/com/instructure/interactions/router/RouterParams.kt +++ b/libs/interactions/src/main/java/com/instructure/interactions/router/RouterParams.kt @@ -24,4 +24,5 @@ object RouterParams { const val PREVIEW = "preview" const val EXTERNAL_ID = "external_id" const val RECENT_ACTIVITY = "view" + const val ATTACHMENT_ID = "attachment_id" } \ No newline at end of file diff --git a/libs/login-api-2/build.gradle b/libs/login-api-2/build.gradle index 2a49d712e0..38a16be117 100644 --- a/libs/login-api-2/build.gradle +++ b/libs/login-api-2/build.gradle @@ -20,7 +20,7 @@ import java.security.MessageDigest apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' -apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.devtools.ksp' apply plugin: 'dagger.hilt.android.plugin' static String isTesting() { @@ -134,7 +134,7 @@ dependencies { implementation Libs.ANDROIDX_FRAGMENT_KTX implementation Libs.HILT - kapt Libs.HILT_COMPILER + ksp Libs.HILT_COMPILER } task copySnickerDoodles(type: Copy) { diff --git a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt index b089c61fd9..6d59908a6c 100644 --- a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt @@ -55,6 +55,12 @@ abstract class LogoutTask( QR_CODE_SWITCH } + companion object { + @Volatile + var isLoggingOut = false + private set + } + protected abstract fun onCleanup() protected abstract fun createLoginIntent(context: Context): Intent protected abstract fun createQRLoginIntent(context: Context, uri: Uri): Intent? @@ -67,6 +73,7 @@ abstract class LogoutTask( @Suppress("EXPERIMENTAL_FEATURE_WARNING") fun execute() { + isLoggingOut = true try { // Get the fcm token to delete the comm channel, then resume logout getFcmToken { registrationId -> diff --git a/libs/pandares/src/main/res/values-ar/strings.xml b/libs/pandares/src/main/res/values-ar/strings.xml index 4ce23feefe..aee24ef716 100644 --- a/libs/pandares/src/main/res/values-ar/strings.xml +++ b/libs/pandares/src/main/res/values-ar/strings.xml @@ -1105,6 +1105,10 @@ ŲØ´Ų„ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„ Ų†ØŦØ­ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„ + ØŦØ§ØąŲ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„ + Ø§ŲƒØĒŲ…Ų„ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„ + ØĨØšŲ„Ø§Ų…Ø§ØĒ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„ + ØĨØšŲ„Ø§Ų…Ø§ØĒ Canvas Ų„ØšŲ…Ų„ŲŠØ§ØĒ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„ Ø§Ų„ØŦØ§ØąŲŠØŠ. Ø§Ų„ØĒŲØ§ØĩŲŠŲ„ Ø§Ų„ŲƒØ§Ų…Ų„ØŠ Ų„ØĒØ§ØąŲŠØŽ Ø§Ų„Ø§ØŗØĒØ­Ų‚Ø§Ų‚ Ø§Ų„ØĨØąØŗØ§Ų„Ø§ØĒ Ø§Ų„Ų…ØąŲŲ‚ @@ -2274,4 +2278,9 @@ Ø§ŲƒØĒب ØŖŲŠØ§Ų… Ø§Ų„ØĒØŖØŽŲŠØą Ø§Ų„ØąØ¯ ØšŲ„Ų‰ Ø§Ų„Ų…ŲˆØļŲˆØš ØąØ¯ŲˆØ¯ ØĨØļØ§ŲŲŠØŠ (%d) + Ø§Ų„ØąØ¯ ØšŲ„Ų‰ Ø§Ų„Ų…ŲˆØļŲˆØš Ų…ØŗØĒØ­Ų‚ + ØąØ¯ŲˆØ¯ ØĨØļØ§ŲŲŠØŠ (%d) Ų…ØŗØĒØ­Ų‚ØŠ + Ų†Ų‚Ø§Øˇ ØĒØ­Ų‚Ų‚ Ø§Ų„Ų†Ų‚Ø§Ø´ + ØĒŲˆØ§ØąŲŠØŽ Ø§ØŗØĒØ­Ų‚Ø§Ų‚ Ų…ØĒؚدد؊ + Ø§Ų†ØĒŲ‡Ų‰ Ø§Ų„Ų…ØŗØ§Ų‚. ؊ØĒØšØ°Øą ØĨØąØŗØ§Ų„ Ø§Ų„ØąØŗØ§ØĻŲ„! diff --git a/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml b/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml index 7112b17c4d..79665ff40b 100644 --- a/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml @@ -1042,6 +1042,10 @@ Download mislykkedes Download lykkedes + Downloader + Download fuldført + Downloadmeddelelser + Canvas-meddelelser for igangvÃĻrende downloads. Fulde oplysninger om afleveringsdato Afleveringer VedhÃĻftet fil @@ -2131,4 +2135,9 @@ Skriv dage forsinket Svar pÃĨ emne Yderligere svar (%d) + Afleveringsdato for svar pÃĨ emne + Yderligere svar (%d) afleveringsdato + Diskussionens kontrolpunkter + Flere forfaldsdatoer + Faget er afsluttet. Kunne ikke sende beskeder! diff --git a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml index 6601073bce..d77fec757e 100644 --- a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml +++ b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml @@ -1042,6 +1042,10 @@ Download failed Download successful + Downloading + Download complete + Download Notifications + Canvas notifications for ongoing downloads. Full due date details Submissions Attachment @@ -2131,4 +2135,9 @@ Write days late Reply to topic Additional replies (%d) + Reply to topic due + Additional replies (%d) due + Discussion Checkpoints + Multiple Due Dates + Subject concluded. Unable to send messages! diff --git a/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml b/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml index 9560caf431..0c74eca2eb 100644 --- a/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml +++ b/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml @@ -1042,6 +1042,10 @@ Download failed Download successful + Downloading + Download complete + Download Notifications + Canvas notifications for ongoing downloads. Full due date details Submissions Attachment @@ -2131,4 +2135,9 @@ Write days late Reply to topic Additional replies (%d) + Reply to topic due + Additional replies (%d) due + Discussion Checkpoints + Multiple Due Dates + Module concluded. Unable to send messages! diff --git a/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml b/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml index c301af2618..bb74410ebc 100644 --- a/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml @@ -1042,6 +1042,10 @@ Kunne ikke laste ned Lastet ned + Laster ned + Nedlasting fullført + Last ned varslinger + Canvas-varslinger for pÃĨgÃĨende nedlastinger. Fulle forfallsdato detaljer Innleveringer Vedlegg @@ -2132,4 +2136,9 @@ Angi dager for sent Svar pÃĨ tema Ekstra svar (%d): + Svar pÃĨ emnet forfaller + Ekstra svar (%d) forfaller + Sjekkpunkter for diskusjon + Flere forfallsdatoer + Fag avsluttet. Kunne ikke sende meldinger! diff --git a/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml b/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml index 9f7a5af55c..837051d5da 100644 --- a/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml @@ -1042,6 +1042,10 @@ Nedladdningen misslyckades Nedladdningen lyckades + Laddar ned + Nedladdning slutfÃļrd + Nedladdningsaviseringar + Canvas-aviseringar fÃļr pÃĨgÃĨende nedladdningar. Fullständiga detaljer fÃļr inlämningsdatum Inskickningar Bilaga @@ -2131,4 +2135,9 @@ Ange dagars fÃļrsening Svar pÃĨ ämne Ytterligare svar (%d) + Svara pÃĨ ämnet som är fÃļrsenat + Ytterligare svar (%d) fÃļrsenade + Kontrollpunkter fÃļr diskussion + Flera inlämningsdatum + Kursen slutfÃļrd Det gick inte att skicka meddelanden! diff --git a/libs/pandares/src/main/res/values-b+zh+HK/strings.xml b/libs/pandares/src/main/res/values-b+zh+HK/strings.xml index 0303341f53..24d6dcee65 100644 --- a/libs/pandares/src/main/res/values-b+zh+HK/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+HK/strings.xml @@ -1026,6 +1026,10 @@ 下čŧ‰å¤ąæ•— 下čŧ‰æˆåŠŸ + æ­Ŗåœ¨ä¸‹čŧ‰ + 厌成下čŧ‰ + 下čŧ‰é€šįŸĨ + 持įēŒä¸‹čŧ‰įš„ Canvas 通įŸĨ。 所有æˆĒæ­ĸæ—ĨæœŸčŠŗæƒ… 提äē¤é …į›Ž 附äģļ @@ -2095,4 +2099,9 @@ ᎍå¯Ģé˛åˆ°å¤Šæ•¸ 回čφä¸ģ題 éĄå¤–å›žčφ (%d) + 回čφä¸ģ題æˆĒæ­ĸ + å…ļäģ–回čφ (%d) æˆĒæ­ĸ + 討čĢ–å€æĒĸæŸĨéģž + 多個æˆĒæ­ĸæ—Ĩ期 + čǞፋįĩæŸã€‚į„Ąæŗ•å‚ŗé€č¨Šæ¯īŧ diff --git a/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml b/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml index 2e83da308c..e20db7e0cc 100644 --- a/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml @@ -1026,6 +1026,10 @@ 下čŊŊå¤ąč´Ĩ 下čŊŊ成功 + æ­Ŗåœ¨ä¸‹čŊŊ + 下čŊŊ厌成 + 下čŊŊ通įŸĨ + Canvas“下čŊŊ中”通įŸĨ。 åŽŒæ•´įš„æˆĒæ­ĸæ—Ĩ期č¯Ļįģ†äŋĄæ¯ 提äē¤éĄš 附äģļ @@ -2095,4 +2099,9 @@ 输å…ĨåģļčŋŸå¤Šæ•° 回复ä¸ģéĸ˜ 更多回复 (%d) + 回复到期ä¸ģéĸ˜ + 更多到期回复 (%d) + 莨čŽēæŖ€æŸĨį‚š + 多ä¸Ē到期æ—Ĩ + č¯žį¨‹åˇ˛į쓿Ÿã€‚æ— æŗ•å‘é€æļˆæ¯īŧ diff --git a/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml b/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml index 0303341f53..24d6dcee65 100644 --- a/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml @@ -1026,6 +1026,10 @@ 下čŧ‰å¤ąæ•— 下čŧ‰æˆåŠŸ + æ­Ŗåœ¨ä¸‹čŧ‰ + 厌成下čŧ‰ + 下čŧ‰é€šįŸĨ + 持įēŒä¸‹čŧ‰įš„ Canvas 通įŸĨ。 所有æˆĒæ­ĸæ—ĨæœŸčŠŗæƒ… 提äē¤é …į›Ž 附äģļ @@ -2095,4 +2099,9 @@ ᎍå¯Ģé˛åˆ°å¤Šæ•¸ 回čφä¸ģ題 éĄå¤–å›žčφ (%d) + 回čφä¸ģ題æˆĒæ­ĸ + å…ļäģ–回čφ (%d) æˆĒæ­ĸ + 討čĢ–å€æĒĸæŸĨéģž + 多個æˆĒæ­ĸæ—Ĩ期 + čǞፋįĩæŸã€‚į„Ąæŗ•å‚ŗé€č¨Šæ¯īŧ diff --git a/libs/pandares/src/main/res/values-ca/strings.xml b/libs/pandares/src/main/res/values-ca/strings.xml index 67df83f0df..f98c805903 100644 --- a/libs/pandares/src/main/res/values-ca/strings.xml +++ b/libs/pandares/src/main/res/values-ca/strings.xml @@ -1042,6 +1042,10 @@ S\'ha produït un error en la baixada S\'ha realitzat correctament la baixada + S\'està baixant + S’ha completat la baixada + Notificacions sobre les baixades + Notificacions del Canvas sobre les baixades en curs. Detalls complets sobre la data de lliurament Entregues Fitxer adjunt @@ -2132,4 +2136,9 @@ Anoteu els dies tard Responeu al tema Respostes addicionals (%d): + Data de lliurament de la resposta al tema + Data de lliurament de les respostes addicionals (%d) + Ítems dels fÃ˛rums + Dates de lliurament mÃēltiples + L’assignatura ha conclÃ˛s. No es poden enviar missatges. diff --git a/libs/pandares/src/main/res/values-cy/strings.xml b/libs/pandares/src/main/res/values-cy/strings.xml index 10ed3a904f..48ec5c139d 100644 --- a/libs/pandares/src/main/res/values-cy/strings.xml +++ b/libs/pandares/src/main/res/values-cy/strings.xml @@ -1042,6 +1042,10 @@ Wedi methu llwytho i lawr Wedi llwyddo i lwytho i lawr + Llwytho i lawr + Wedi gorffen llwytho i lawr + Hysbysiadau llwytho i lawr + Hysbysiadau Canvas ar gyfer ffeiliau sy’n cael eu llwytho i lawr ar hyn o bryd. Manylion llawn dyddiad erbyn Cyflwyniadau Atodiad @@ -2131,4 +2135,9 @@ Rhowch nifer y \'diwrnodau yn hwyr\'. Ateb pwnc Ymatebion ychwanegol (%d) + Ymateb i’r pwnc erbyn + Ymatebion ychwanegol (%d) erbyn + Pwyntiau Gwirio Trafodaeth + Mwy nag un dyddiad cau + Cwrs wedi dod i ben. Methu anfon negeseuon! diff --git a/libs/pandares/src/main/res/values-da/strings.xml b/libs/pandares/src/main/res/values-da/strings.xml index f32d08b3ff..18115f6f86 100644 --- a/libs/pandares/src/main/res/values-da/strings.xml +++ b/libs/pandares/src/main/res/values-da/strings.xml @@ -1042,6 +1042,10 @@ Download mislykkedes Download lykkedes + Downloader + Download fuldført + Downloadmeddelelser + Canvas-meddelelser for igangvÃĻrende downloads. Fulde oplysninger om afleveringsdato Afleveringer VedhÃĻftet fil @@ -2131,4 +2135,9 @@ Skriv dage forsinket Svar pÃĨ emne Yderligere svar (%d) + Afleveringsdato for svar pÃĨ emne + Yderligere svar (%d) afleveringsdato + Diskussionens kontrolpunkter + Flere forfaldsdatoer + Faget er afsluttet. Kunne ikke sende beskeder! diff --git a/libs/pandares/src/main/res/values-de/strings.xml b/libs/pandares/src/main/res/values-de/strings.xml index c2b0c9ad77..52cd25e6d0 100644 --- a/libs/pandares/src/main/res/values-de/strings.xml +++ b/libs/pandares/src/main/res/values-de/strings.xml @@ -1042,6 +1042,10 @@ Download fehlgeschlagen Download erfolgreich + Wird heruntergeladen + Download abgeschlossen + Download-Benachrichtigungen + Canvas-Benachrichtigungen fÃŧr laufende Downloads. Vollständige Details zum Abgabedatum Übermittlungen Anhang @@ -2131,4 +2135,9 @@ Verspätete Tage eingeben Auf Thema antworten Zusätzliche Antworten (%d) + Antwort auf Thema fällig + Zusätzliche Antworten (%d) fällig + Kontrollpunkte fÃŧr Diskussionen + Mehrere Abgabetermine + Kurs abgeschlossen. Nachrichten kÃļnnen nicht gesendet werden! diff --git a/libs/pandares/src/main/res/values-en-rAU/strings.xml b/libs/pandares/src/main/res/values-en-rAU/strings.xml index 926f0415b9..12c9b460be 100644 --- a/libs/pandares/src/main/res/values-en-rAU/strings.xml +++ b/libs/pandares/src/main/res/values-en-rAU/strings.xml @@ -1042,6 +1042,10 @@ Download failed Download successful + Downloading + Download complete + Download Notifications + Canvas notifications for ongoing downloads. Full due date details Submissions Attachment @@ -2131,4 +2135,9 @@ Write days late Reply to topic Additional replies (%d) + Reply to topic due + Additional replies (%d) due + Discussion Checkpoints + Multiple Due Dates + Course concluded. Unable to send messages! diff --git a/libs/pandares/src/main/res/values-en-rCY/strings.xml b/libs/pandares/src/main/res/values-en-rCY/strings.xml index 9560caf431..0c74eca2eb 100644 --- a/libs/pandares/src/main/res/values-en-rCY/strings.xml +++ b/libs/pandares/src/main/res/values-en-rCY/strings.xml @@ -1042,6 +1042,10 @@ Download failed Download successful + Downloading + Download complete + Download Notifications + Canvas notifications for ongoing downloads. Full due date details Submissions Attachment @@ -2131,4 +2135,9 @@ Write days late Reply to topic Additional replies (%d) + Reply to topic due + Additional replies (%d) due + Discussion Checkpoints + Multiple Due Dates + Module concluded. Unable to send messages! diff --git a/libs/pandares/src/main/res/values-en-rGB/strings.xml b/libs/pandares/src/main/res/values-en-rGB/strings.xml index 536b3b0ebc..dfea3dcd76 100644 --- a/libs/pandares/src/main/res/values-en-rGB/strings.xml +++ b/libs/pandares/src/main/res/values-en-rGB/strings.xml @@ -1042,6 +1042,10 @@ Download failed Download successful + Downloading + Download complete + Download Notifications + Canvas notifications for ongoing downloads. Full due date details Submissions Attachment @@ -2131,4 +2135,9 @@ Write days late Reply to topic Additional replies (%d) + Reply to topic due + Additional replies (%d) due + Discussion Checkpoints + Multiple Due Dates + Course concluded. Unable to send messages! diff --git a/libs/pandares/src/main/res/values-en/strings.xml b/libs/pandares/src/main/res/values-en/strings.xml index f0c17c884a..f48c6ca568 100644 --- a/libs/pandares/src/main/res/values-en/strings.xml +++ b/libs/pandares/src/main/res/values-en/strings.xml @@ -1043,6 +1043,10 @@ Downloading Download failed Download successful + Downloading + Download complete + Download Notifications + Canvas notifications for ongoing downloads. Full due date details Submissions @@ -2151,4 +2155,9 @@ Write days late Reply to topic Additional replies (%d) + Reply to topic due + Additional replies (%d) due + Discussion Checkpoints + Multiple Due Dates + Course concluded. Unable to send messages! diff --git a/libs/pandares/src/main/res/values-es-rES/strings.xml b/libs/pandares/src/main/res/values-es-rES/strings.xml index 8472feb736..dd89f1c2f8 100644 --- a/libs/pandares/src/main/res/values-es-rES/strings.xml +++ b/libs/pandares/src/main/res/values-es-rES/strings.xml @@ -1042,6 +1042,10 @@ No se ha podido realizar la descarga La descarga se ha realizado correctamente + Descargando + Descarga completa + Descargar notificaciones + Notificaciones de Canvas para descargas en curso. Detalles completos de la fecha de entrega Entregas Archivo adjunto @@ -2133,4 +2137,9 @@ Escribir los días de retraso Responder al tema Respuestas adicionales (%d) + Responder al tema pendiente + Respuestas adicionales (%d) pendientes + Puntos de comprobaciÃŗn del foro de discusiÃŗn + Varias fechas de entrega + Asignatura concluida. ÂĄNo se pueden enviar mensajes! diff --git a/libs/pandares/src/main/res/values-es/strings.xml b/libs/pandares/src/main/res/values-es/strings.xml index a0a8e6a253..a949ff77be 100644 --- a/libs/pandares/src/main/res/values-es/strings.xml +++ b/libs/pandares/src/main/res/values-es/strings.xml @@ -1042,6 +1042,10 @@ No se pudo realizar la descarga La descarga se ha realizado correctamente + Descargando + Descarga completada + Notificaciones de descarga + Notificaciones en Canvas sobre descargas en curso. Detalles completos de la fecha de entrega Entregas Archivo adjunto @@ -2131,4 +2135,9 @@ Escribir los días de atraso Responder al tema Respuestas adicionales (%d) + Respuesta al tema pendiente + Respuestas adicionales (%d) pendientes + Puntos de control del foro de discusiÃŗn + Varias fechas límite + Curso finalizado. ÂĄNo se pueden enviar mensajes! diff --git a/libs/pandares/src/main/res/values-fi/strings.xml b/libs/pandares/src/main/res/values-fi/strings.xml index 22549a2402..342dcf39a9 100644 --- a/libs/pandares/src/main/res/values-fi/strings.xml +++ b/libs/pandares/src/main/res/values-fi/strings.xml @@ -1042,6 +1042,10 @@ Lataus epäonnistui Lataus onnistui + Ladataan + Lataus valmis + Latausilmoitukset + Canvas-ilmoitukset meneillään oleville latauksille. Täydelliset palautuspäivää koskevat tiedot Lähetykset Liite @@ -2131,4 +2135,9 @@ Kirjoita myÃļhässäolopäivien määrä Vastaa aiheeseen Ylimääräiset vastaukset (%d) + Vastaa aiheeseen määräpäivään mennessä + Lisävastausten määräpäivä (%d) + Keskustelun tarkistuskohdat + Useita määräpäiviä + Kurssi on päättynyt. Viestien lähetys ei onnistu! diff --git a/libs/pandares/src/main/res/values-fr-rCA/strings.xml b/libs/pandares/src/main/res/values-fr-rCA/strings.xml index f38265aa93..1436c7d6e8 100644 --- a/libs/pandares/src/main/res/values-fr-rCA/strings.xml +++ b/libs/pandares/src/main/res/values-fr-rCA/strings.xml @@ -1042,6 +1042,10 @@ Erreur de tÊlÊchargement TÊlÊchargement effectuÊ avec succès + TÊlÊchargement en cours… + TÊlÊchargement terminÊ + TÊlÊcharger les notifications + Notifications de Canvas pour les tÊlÊchargements en cours. DÊtails complets de la date d\'ÊchÊance Envois Pièce jointe @@ -2131,4 +2135,9 @@ Écrire avec des jours de retard RÊpondre au sujet RÊponses supplÊmentaires (%d) + RÊponse au sujet dÃģ + RÊponses complÊmentaires (%d) attendues + Points de contrôle de la discussion + Dates limites multiples + Cours achevÊ. Impossible d’envoyer des messages! diff --git a/libs/pandares/src/main/res/values-fr/strings.xml b/libs/pandares/src/main/res/values-fr/strings.xml index 8d75da0bb8..89c620ba7c 100644 --- a/libs/pandares/src/main/res/values-fr/strings.xml +++ b/libs/pandares/src/main/res/values-fr/strings.xml @@ -1042,6 +1042,10 @@ Échec du tÊlÊchargement TÊlÊchargement rÊussi + TÊlÊchargement + TÊlÊchargement terminÊ + TÊlÊcharger les notifications + Notifications Canvas pour les tÊlÊchargements en cours. DÊtails complets de la date limite de rendu Soumissions Pièce jointe @@ -2131,4 +2135,9 @@ Jours de retard d’Êcriture RÊpondre au sujet RÊponses supplÊmentaires (%d) + RÊponse au sujet attendue + RÊponses supplÊmentaires (%d) attendues + Points de contrôle des discussions + Dates limites multiples + Cours terminÊs. Impossible d’envoyer des messages ! diff --git a/libs/pandares/src/main/res/values-ga/strings.xml b/libs/pandares/src/main/res/values-ga/strings.xml index 2caef1f4cb..8587676c29 100644 --- a/libs/pandares/src/main/res/values-ga/strings.xml +++ b/libs/pandares/src/main/res/values-ga/strings.xml @@ -1043,6 +1043,10 @@ Theip ar an íoslÃŗdÃĄil ÍoslÃŗdÃĄil an rath + Ag íoslÃŗdÃĄil + ÍoslÃŗdÃĄil críochnaithe + FÃŗgraí ÍoslÃŗdÃĄla + FÃŗgraí CanbhÃĄs le haghaidh íoslÃŗdÃĄlacha atÃĄ ar siÃēl. Sonraí iomlÃĄna faoin dÃĄta dlite UaslÃŗdÃĄlacha CeangaltÃĄn @@ -2143,4 +2147,12 @@ Leacaigh NÃĄ bac leis an Mac LÊinn Níl tuilleadh sonraí againn don Mhac LÊinn agus don Tasc seo + Scríobh lÃĄ dÊanach + Freagra ar an ÃĄbhar + Freagraí breise (%d) + Freagra ar an topaic dlite + Freagraí breise (%d) dlite + Seicphointí FÃŗraim + DÃĄta Dlite Iolracha + Críochnaíodh an cÃērsa. Ní fÊidir teachtaireachtaí a sheoladh! diff --git a/libs/pandares/src/main/res/values-hi/strings.xml b/libs/pandares/src/main/res/values-hi/strings.xml index ae4b296129..d37c3a2b0d 100644 --- a/libs/pandares/src/main/res/values-hi/strings.xml +++ b/libs/pandares/src/main/res/values-hi/strings.xml @@ -1042,6 +1042,10 @@ ā¤Ąā¤žā¤‰ā¤¨ā¤˛āĨ‹ā¤Ą ā¤•ā¤°ā¤¨ā¤ž ā¤ĩā¤ŋā¤Ģ⤞ ā¤°ā¤šā¤ž ā¤Ąā¤žā¤‰ā¤¨ā¤˛āĨ‹ā¤Ą ā¤•ā¤°ā¤¨ā¤ž ⤏ā¤Ģ⤞ ā¤°ā¤šā¤ž + ā¤Ąā¤žā¤‰ā¤¨ā¤˛āĨ‹ā¤Ą ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤ž ā¤°ā¤šā¤ž ā¤šāĨˆ + ā¤Ąā¤žā¤‰ā¤¨ā¤˛āĨ‹ā¤Ą ā¤ĒāĨ‚ā¤°ā¤ž ā¤šāĨā¤† + ⤏āĨ‚ā¤šā¤¨ā¤žā¤ā¤‚ ā¤Ąā¤žā¤‰ā¤¨ā¤˛āĨ‹ā¤Ą ⤕⤰āĨ‡ā¤‚ + ā¤œā¤žā¤°āĨ€ ā¤Ąā¤žā¤‰ā¤¨ā¤˛āĨ‹ā¤ĄāĨā¤¸ ⤕āĨ‡ ⤞ā¤ŋā¤ Canvas ⤏āĨ‚ā¤šā¤¨ā¤žā¤ā¤‚āĨ¤ ⤍ā¤ŋ⤝⤤ ⤤ā¤ŋā¤Ĩā¤ŋ ā¤•ā¤ž ā¤ĒāĨ‚ā¤°ā¤ž ā¤ŦāĨā¤¯āĨŒā¤°ā¤ž ⤏ā¤Ŧā¤Žā¤ŋā¤ļ⤍ ⤏⤂⤞⤗āĨā¤¨ā¤• @@ -2139,4 +2143,9 @@ \'ā¤Ļā¤ŋ⤍ ā¤ĻāĨ‡ā¤° ⤏āĨ‡\' ⤞ā¤ŋ⤖āĨ‡ā¤‚ ā¤ĩā¤ŋ⤎⤝ ā¤Ē⤰ ⤉⤤āĨā¤¤ā¤° ā¤ĻāĨ‡ā¤‚ ⤅⤤ā¤ŋ⤰ā¤ŋ⤕āĨā¤¤ ⤉⤤āĨā¤¤ā¤° (%d) + ā¤ĩā¤ŋ⤎⤝ ā¤•ā¤ž ⤉⤤āĨā¤¤ā¤° ⤍ā¤ŋ⤝⤤ ā¤šāĨˆ + ⤅⤤ā¤ŋ⤰ā¤ŋ⤕āĨā¤¤ ⤉⤤āĨā¤¤ā¤° (%d) ⤍ā¤ŋ⤝⤤ ā¤šāĨˆ + ⤚⤰āĨā¤šā¤ž ā¤œā¤žā¤‚ā¤š ā¤Ŧā¤ŋ⤂ā¤ĻāĨ + ā¤ā¤•ā¤žā¤§ā¤ŋ⤕ ⤍ā¤ŋ⤝⤤ ⤤ā¤ŋā¤Ĩā¤ŋā¤¯ā¤žā¤‚ + ā¤Ēā¤žā¤ āĨā¤¯ā¤•āĨā¤°ā¤Ž ā¤¸ā¤Žā¤žā¤ĒāĨā¤¤ ā¤šāĨā¤†āĨ¤ ⤏⤂ā¤ĻāĨ‡ā¤ļ ⤭āĨ‡ā¤œā¤¨āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ! diff --git a/libs/pandares/src/main/res/values-ht/strings.xml b/libs/pandares/src/main/res/values-ht/strings.xml index d7626f6b6c..98f545211e 100644 --- a/libs/pandares/src/main/res/values-ht/strings.xml +++ b/libs/pandares/src/main/res/values-ht/strings.xml @@ -1042,6 +1042,10 @@ Echèk telechajan Telechajman reyisi + Telechajman + Telechajman fini + Avi Telechajman + Notifikasyon Canvas pou telechajman k ap fèt yo Tout detay delè Soumisyon Atachman @@ -2131,4 +2135,9 @@ Ekri jou reta yo Reponn a sijè Plis repons (%d) + Reponn sijè a anvan dat limit + Plis repons (%d) nan dat limit + Pwen KontwÃ˛l Diskisyon yo + Plizyè Delè + Kou a fini. Enposib pou voye mesaj! diff --git a/libs/pandares/src/main/res/values-id/strings.xml b/libs/pandares/src/main/res/values-id/strings.xml index 7dbe7147bf..a2ad617936 100644 --- a/libs/pandares/src/main/res/values-id/strings.xml +++ b/libs/pandares/src/main/res/values-id/strings.xml @@ -1042,6 +1042,10 @@ Pengunduhan gagal Unduhan berhasil + Mengunduh + Pengunduhan selesai + Notifikasi Pengunduhan + Notifikasi Canvas untuk pengunduhan yang sedang berlangsung. Detail tanggal batas lengkap Penyerahan Lampiran @@ -2134,4 +2138,9 @@ Tuliskan hari terlambat Balas ke topik Balasan tambahan (%d) + Tanggal batas balas ke topik + Tanggal batas balasan tambahan (%d) + Checkpoint Diskusi + Tanggal Batas Berganda + Kursus selesai. Tidak dapat mengirim pesan! diff --git a/libs/pandares/src/main/res/values-is/strings.xml b/libs/pandares/src/main/res/values-is/strings.xml index 029cb55faa..3bbe14e16f 100644 --- a/libs/pandares/src/main/res/values-is/strings.xml +++ b/libs/pandares/src/main/res/values-is/strings.xml @@ -468,7 +468,7 @@ HÃŗpskilaboð? BÃĻta Ãļllum við eitt hÃŗpsamtal eða senda skilaboð ÃĄ alla hvern fyrir sig? - HÃŗpur + Verkefnaflokkur HÃŗpar HÃŗpmeðlimir @@ -987,7 +987,7 @@ UppfÃĻri einingaupplÃŊsingar… Bíður mats %s punktar - Heildarpunktar + Heildarstig %s punktar Einkunn %s / %s punktar @@ -1042,6 +1042,10 @@ Niðurhal mistÃŗkst Niðurhal tÃŗkst + Hleðst niður + Niðurhleðslu lokið + Niðurhalstilkynningar + Canvas-tilkynningar fyrir niðurhal sem er í gangi. UpplÃŊsingar um skiladag Skil Viðhengi @@ -1896,7 +1900,7 @@ Byggt ÃĄ verkefnum með einkunnum Seint Skiladagur - HÃŗpur + Verkefnaflokkur KjÃļrstillingar fyrir einkunnir Einkunnatímabil Raða eftir @@ -2112,7 +2116,7 @@ UpplÃŊsingar um skil Hleður skilaupplÃŊsingum OrðafjÃļldi - Matskvarði + Matskvarðar Einkunn Skrifaðu einkunn hÊr Athugasemdir (%s) @@ -2131,4 +2135,9 @@ Skrifa dÃļgum of seint Svar við efni ViðbÃŗtarsvÃļr (%d) + Komið að svari við efni + Komið að viðbÃŗtarsvÃļrum (%d) + UmrÃĻðuvÃļrður + Margir skiladagar + NÃĄmskeiði lokið. Ekki var hÃĻgt að senda skilaboð! diff --git a/libs/pandares/src/main/res/values-it/strings.xml b/libs/pandares/src/main/res/values-it/strings.xml index 881e0e6678..8411798b12 100644 --- a/libs/pandares/src/main/res/values-it/strings.xml +++ b/libs/pandares/src/main/res/values-it/strings.xml @@ -1042,6 +1042,10 @@ Download non riuscito Download eseguito correttamente + Download in corso + Download completato + Notifiche download + Notifiche Canvas per download continui. Dettagli data di scadenza completi Consegne Allegato @@ -2131,4 +2135,9 @@ Scrivi giorni di ritardo Rispondi all\'argomento Risposte aggiuntive (%d) + Scadenza Rispondi all\'argomento + Scadenza Risposte aggiuntive (%d) + Punti di controllo discussione + PiÚ date di scadenza + Corso concluso. Impossibile inviare messaggi! diff --git a/libs/pandares/src/main/res/values-ja/strings.xml b/libs/pandares/src/main/res/values-ja/strings.xml index aebd2c8772..b44f3f6ed3 100644 --- a/libs/pandares/src/main/res/values-ja/strings.xml +++ b/libs/pandares/src/main/res/values-ja/strings.xml @@ -1026,6 +1026,10 @@ ダã‚Ļãƒŗãƒ­ãƒŧドãĢå¤ąæ•— ダã‚Ļãƒŗãƒ­ãƒŧドãĢ成功 + čĒ­ãŋčžŧãŋ中 + ダã‚Ļãƒŗãƒ­ãƒŧド厌äē† + 通įŸĨをダã‚Ļãƒŗãƒ­ãƒŧドする + įžåœ¨ãƒ€ã‚Ļãƒŗãƒ­ãƒŧド中ぎ Canvas 通įŸĨ。 įˇ ã‚åˆ‡ã‚Šæ—ĨãŽå…¨čŠŗį´° 提å‡ēį‰Š æˇģäģ˜ãƒ•ã‚Ąã‚¤ãƒĢ @@ -2095,4 +2099,9 @@ 遅åģļæ—Ĩ数を書いãĻください トピックãĢčŋ”äŋĄã™ã‚‹ čŋŊ加ぎčŋ”äŋĄ (%d) + トピックãĢčŋ”äŋĄã™ã‚‹æœŸæ—Ĩīŧš + čŋŊ加ぎčŋ”äŋĄ (%d)ぎ期æ—Ĩīŧš + ãƒ‡ã‚Ŗã‚šã‚Ģãƒƒã‚ˇãƒ§ãƒŗãƒã‚§ãƒƒã‚¯ãƒã‚¤ãƒŗãƒˆ + č¤‡æ•°ãŽįˇ åˆ‡æ—Ĩ + ã‚ŗãƒŧ゚厌äē†ã—ãžã—ãŸã€‚ãƒĄãƒƒã‚ģãƒŧジを送äŋĄã§ããžã›ã‚“īŧ diff --git a/libs/pandares/src/main/res/values-mi/strings.xml b/libs/pandares/src/main/res/values-mi/strings.xml index ccd0616cc4..299039ec62 100644 --- a/libs/pandares/src/main/res/values-mi/strings.xml +++ b/libs/pandares/src/main/res/values-mi/strings.xml @@ -1042,6 +1042,10 @@ I rahua te tikiake I pai te tikiake + Tikiake ana + Kua oti te tango + Ngā Pānui Tikiake + Ngā pānui Canvas mō ngā tikiake tonu. Ngā taipitopito katoa o ngā rā e tika ana Tukunga Āpitihanga @@ -2131,4 +2135,9 @@ Tuhia nga ra tomuri Whakautu ki te kaupapa Nga whakautu taapiri (%d) + Kua tae ki te wā whakautu ki te kaupapa + Kua tae ki te wā whakautu tāpiri (%d) + Matapakinga Takitaki + Maha Rā e tika ana + Kua mutu te akoranga. Kaore e taea te tuku karere! diff --git a/libs/pandares/src/main/res/values-ms/strings.xml b/libs/pandares/src/main/res/values-ms/strings.xml index d1c5643f76..e4f8ff0795 100644 --- a/libs/pandares/src/main/res/values-ms/strings.xml +++ b/libs/pandares/src/main/res/values-ms/strings.xml @@ -1042,6 +1042,10 @@ Muat turun gagal Muat turun berjaya + Memuat turun + Muat turun lengkap + Pemberitahuan muat turun + Pemberitahuan Canvas untuk muat turun yang sedang berjalan. Butiran tarikh siap yang penuh Serahan Kepilan @@ -2137,4 +2141,9 @@ Tulis hari yang lewat Balas kepada topik Balasan tambahan (%d) + Balas kepada topik yang perlu dihantar + Balasan tambahan (%d) perlu dihantar + Titik Semakan Perbincangan + Berbilang Tarikh Siap + Kursus selesaai. Tidak dapat menghantar mesej! diff --git a/libs/pandares/src/main/res/values-nb/strings.xml b/libs/pandares/src/main/res/values-nb/strings.xml index dd0a17607a..5aaea6f447 100644 --- a/libs/pandares/src/main/res/values-nb/strings.xml +++ b/libs/pandares/src/main/res/values-nb/strings.xml @@ -1042,6 +1042,10 @@ Kunne ikke laste ned Lastet ned + Laster ned + Nedlasting fullført + Last ned varslinger + Canvas-varslinger for pÃĨgÃĨende nedlastinger. Fulle forfallsdato detaljer Innleveringer Vedlegg @@ -2132,4 +2136,9 @@ Angi dager for sent Svar pÃĨ tema Ekstra svar (%d): + Svar pÃĨ emnet forfaller + Ekstra svar (%d) forfaller + Sjekkpunkter for diskusjon + Flere forfallsdatoer + Emne avsluttet. Kunne ikke sende meldinger! diff --git a/libs/pandares/src/main/res/values-nl/strings.xml b/libs/pandares/src/main/res/values-nl/strings.xml index 94ef76be91..fcdb3f2d84 100644 --- a/libs/pandares/src/main/res/values-nl/strings.xml +++ b/libs/pandares/src/main/res/values-nl/strings.xml @@ -1042,6 +1042,10 @@ Downloaden mislukt Downloaden gelukt + Bezig met downloaden + Downloaden voltooid + Meldingen downloaden + Canvas-meldingen voor lopende downloads. Alle details inleverdatum Inleveringen Bijlage @@ -2131,4 +2135,9 @@ Schrijf het aantal dagen te laat op Reageer op onderwerp Aanvullende reacties (%d) + Antwoord op onderwerp vereist + Aanvullende antwoorden (%d) vereist + Discussiecontrolemomenten + Meerdere inleverdatums + Cursus afgesloten. Kan geen berichten verzenden! diff --git a/libs/pandares/src/main/res/values-pl/strings.xml b/libs/pandares/src/main/res/values-pl/strings.xml index 5e755b9629..0df4c7b4ef 100644 --- a/libs/pandares/src/main/res/values-pl/strings.xml +++ b/libs/pandares/src/main/res/values-pl/strings.xml @@ -1074,6 +1074,10 @@ Nie udało się pobrać Pobrano + Pobieranie + Ukończono pobieranie + Powiadomienia o pobraniu + Powiadomienia Canvas dotyczące pobieranych obecnie plikÃŗw. SzczegÃŗÅ‚y dotyczące terminu Przesłane materiały Załącznik @@ -2203,4 +2207,9 @@ Wpisz dni spÃŗÅēnienia OdpowiedÅē na temat Dodatkowe odpowiedzi (%d) + Wymagana odpowiedÅē na temat + Wymagane dodatkowe odpowiedzi (%d) + Punkty kontrolne dyskusji + Wiele terminÃŗw + Zakończony kurs. Nie udało się wysłać wiadomości! diff --git a/libs/pandares/src/main/res/values-pt-rBR/strings.xml b/libs/pandares/src/main/res/values-pt-rBR/strings.xml index 1df263593f..d4a79de73f 100644 --- a/libs/pandares/src/main/res/values-pt-rBR/strings.xml +++ b/libs/pandares/src/main/res/values-pt-rBR/strings.xml @@ -1042,6 +1042,10 @@ Falha do download Download bem-sucedido + Baixando + Download concluído + Baixar notificaçÃĩes + NotificaçÃĩes do Canvas para downloads em curso. Detalhes completos do prazo de entrega Envios Anexo @@ -2131,4 +2135,9 @@ Escrever com dias de atraso Responder ao tÃŗpico Respostas adicionais (%d) + Responder ao tÃŗpico devido + Respostas adicionais (%d) devidas + Checkpoints de discussÃŖo + VÃĄrias datas de entrega + Curso concluído. NÃŖo Ê possível enviar mensagens! diff --git a/libs/pandares/src/main/res/values-pt-rPT/strings.xml b/libs/pandares/src/main/res/values-pt-rPT/strings.xml index a229f1cb1c..08fe159d38 100644 --- a/libs/pandares/src/main/res/values-pt-rPT/strings.xml +++ b/libs/pandares/src/main/res/values-pt-rPT/strings.xml @@ -1042,6 +1042,10 @@ TransferÃĒncia falhou TransferÃĒncia com ÃĒxito + A transferir + TransferÃĒncia concluída + NotificaçÃĩes de transferÃĒncia + NotificaçÃĩes do Canvas para transferÃĒncias em andamento. Detalhes completos da data limite SubmissÃĩes Anexo @@ -2131,4 +2135,9 @@ Escrever com atraso Responder ao tÃŗpico Respostas adicionais (%d) + Resposta ao tÃŗpico com prazo + Respostas adicionais (%d) com prazo + Pontos de controlo do debate + Datas de limite mÃēltiplas + Disciplina concluída. NÃŖo Ê possível enviar mensagens! diff --git a/libs/pandares/src/main/res/values-ru/strings.xml b/libs/pandares/src/main/res/values-ru/strings.xml index 63ec96d1c4..b323837063 100644 --- a/libs/pandares/src/main/res/values-ru/strings.xml +++ b/libs/pandares/src/main/res/values-ru/strings.xml @@ -1074,6 +1074,10 @@ ХйОК ҁĐēĐ°Ņ‡Đ¸Đ˛Đ°ĐŊĐ¸Ņ ĐĄĐēĐ°Ņ‡Đ¸Đ˛Đ°ĐŊиĐĩ СавĐĩŅ€ŅˆĐĩĐŊĐž + ĐĄĐēĐ°Ņ‡Đ¸Đ˛Đ°ĐŊиĐĩ + ĐĄĐēĐ°Ņ‡Đ¸Đ˛Đ°ĐŊиĐĩ СавĐĩŅ€ŅˆĐĩĐŊĐž + ĐŖĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊĐ¸Ņ Đž ҁĐēĐ°Ņ‡Đ¸Đ˛Đ°ĐŊии + ĐŖĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊĐ¸Ņ Canvas Đ´ĐģŅ Ņ‚ĐĩĐēŅƒŅ‰Đ¸Ņ… ҁĐēĐ°Ņ‡Đ¸Đ˛Đ°ĐŊиК. ПоĐģĐŊĐ°Ņ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Đž ŅŅ€ĐžĐēĐĩ Đ˛Ņ‹ĐŋĐžĐģĐŊĐĩĐŊĐ¸Ņ ĐžŅ‚ĐŋŅ€Đ°Đ˛Đēи ВĐģĐžĐļĐĩĐŊиĐĩ @@ -2203,4 +2207,9 @@ НаĐŋĐ¸ŅĐ°Ņ‚ŅŒ Đ´ĐŊи ĐžĐŋОСдаĐŊĐ¸Ņ ĐžŅ‚Đ˛ĐĩŅ‚ ĐŋĐž Ņ‚ĐĩĐŧĐĩ ДоĐŋĐžĐģĐŊĐ¸Ņ‚ĐĩĐģҌĐŊŅ‹Đĩ ĐžŅ‚Đ˛Đĩ҂ҋ (%d) + ĐĸŅ€ĐĩĐąŅƒĐĩŅ‚ŅŅ ĐžŅ‚Đ˛ĐĩŅ‚ ĐŋĐž Ņ‚ĐĩĐŧĐĩ + ĐĸŅ€ĐĩĐąŅƒŅŽŅ‚ŅŅ Đ´ĐžĐŋĐžĐģĐŊĐ¸Ņ‚ĐĩĐģҌĐŊŅ‹Đĩ ĐžŅ‚Đ˛Đĩ҂ҋ (%d) + КоĐŊŅ‚Ņ€ĐžĐģҌĐŊŅ‹Đĩ Ņ‚ĐžŅ‡Đēи ĐžĐąŅŅƒĐļĐ´ĐĩĐŊĐ¸Ņ + МĐŊĐžĐļĐĩŅŅ‚Đ˛ĐĩĐŊĐŊŅ‹Đĩ Đ´Đ°Ņ‚Ņ‹ Đ˛Ņ‹ĐŋĐžĐģĐŊĐĩĐŊĐ¸Ņ + ĐšŅƒŅ€Ņ СавĐĩŅ€ŅˆĐĩĐŊ. НĐĩвОСĐŧĐžĐļĐŊĐž ĐžŅ‚ĐŋŅ€Đ°Đ˛Đ¸Ņ‚ŅŒ ŅĐžĐžĐąŅ‰ĐĩĐŊиĐĩ! diff --git a/libs/pandares/src/main/res/values-sl/strings.xml b/libs/pandares/src/main/res/values-sl/strings.xml index 848329dbf1..1bb2795c5f 100644 --- a/libs/pandares/src/main/res/values-sl/strings.xml +++ b/libs/pandares/src/main/res/values-sl/strings.xml @@ -1042,6 +1042,10 @@ Prenos ni uspel. Prenos je uspel. + PrenaÅĄanje + Prenos je končan + Sporočila o prenosih + Obvestila sistema Canvas za prenose v teku. Vse podrobnosti o roku Oddaje Priloga @@ -2131,4 +2135,9 @@ Vnesite dni zamude Odgovor na temo Dodatni odgovori (%d) + Rok za odgovor na temo + Rok za dodatne odgovore (%d) + Točke preverjanja razprave + Več rokov + Predmet zaključen. Sporočil ni bilo mogoče poslati! diff --git a/libs/pandares/src/main/res/values-sv/strings.xml b/libs/pandares/src/main/res/values-sv/strings.xml index 2aef875d78..225951db91 100644 --- a/libs/pandares/src/main/res/values-sv/strings.xml +++ b/libs/pandares/src/main/res/values-sv/strings.xml @@ -1042,6 +1042,10 @@ Nedladdningen misslyckades Nedladdningen lyckades + Laddar ned + Nedladdning slutfÃļrd + Nedladdningsaviseringar + Canvas-aviseringar fÃļr pÃĨgÃĨende nedladdningar. Fullständiga detaljer fÃļr inlämningsdatum Inskickningar Bilaga @@ -2131,4 +2135,9 @@ Ange dagars fÃļrsening Svar pÃĨ ämne Ytterligare svar (%d) + Svara pÃĨ ämnet som är fÃļrsenat + Ytterligare svar (%d) fÃļrsenade + Kontrollpunkter fÃļr diskussion + Flera inlämningsdatum + Kursen slutfÃļrd Det gick inte att skicka meddelanden! diff --git a/libs/pandares/src/main/res/values-th/strings.xml b/libs/pandares/src/main/res/values-th/strings.xml index 16da97124e..46817a7b8f 100644 --- a/libs/pandares/src/main/res/values-th/strings.xml +++ b/libs/pandares/src/main/res/values-th/strings.xml @@ -1042,6 +1042,10 @@ ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧ ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”āš€ā¸Ēā¸Ŗāš‡ā¸ˆā¸Ēā¸´āš‰ā¸™ + ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ + ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”āš€ā¸Ēā¸Ŗāš‡ā¸ˆā¸Ēā¸´āš‰ā¸™ + ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ⏗ā¸ĩāšˆāšā¸ˆāš‰ā¸‡ + ā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ⏂⏭⏇ Canvas ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔⏗ā¸ĩāšˆā¸ā¸ŗā¸Ĩā¸ąā¸‡ā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗ ⏪⏞ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸā¸”ā¸§ā¸ąā¸™ā¸„ā¸Ŗā¸šā¸ā¸ŗā¸Ģā¸™ā¸”ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸” ⏜ā¸Ĩā¸‡ā¸˛ā¸™ā¸ˆā¸ąā¸”ā¸Ēāšˆā¸‡ āš€ā¸­ā¸ā¸Ēā¸˛ā¸Ŗāšā¸™ā¸š @@ -2131,4 +2135,9 @@ āš€ā¸‚ā¸ĩā¸ĸā¸™ā¸ˆā¸ŗā¸™ā¸§ā¸™ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆā¸Ĩāšˆā¸˛ā¸Šāš‰ā¸˛ ā¸•ā¸­ā¸šā¸ā¸Ĩā¸ąā¸šāš„ā¸›ā¸ĸā¸ąā¸‡ā¸Ģā¸ąā¸§ā¸‚āš‰ā¸­ ā¸ā¸˛ā¸Ŗā¸•ā¸­ā¸šā¸ā¸Ĩā¸ąā¸šāš€ā¸žā¸´āšˆā¸Ąāš€ā¸•ā¸´ā¸Ą (%d) + ⏁⏺ā¸Ģā¸™ā¸”āš€ā¸§ā¸Ĩā¸˛ā¸•ā¸­ā¸šā¸ā¸Ĩā¸ąā¸šāš„ā¸›ā¸ĸā¸ąā¸‡ā¸Ģā¸ąā¸§ā¸‚āš‰ā¸­ + ⏁⏺ā¸Ģā¸™ā¸”āš€ā¸§ā¸Ĩā¸˛ā¸•ā¸­ā¸šā¸ā¸Ĩā¸ąā¸šāš€ā¸žā¸´āšˆā¸Ąāš€ā¸•ā¸´ā¸Ą (%d) + ā¸ˆā¸¸ā¸”ā¸•ā¸Ŗā¸§ā¸ˆā¸Ē⏭⏚ā¸Ēāšˆā¸§ā¸™ā¸ā¸˛ā¸Ŗā¸žā¸šā¸”ā¸„ā¸¸ā¸ĸ + ā¸§ā¸ąā¸™ā¸„ā¸Ŗā¸šā¸ā¸ŗā¸Ģ⏙⏔ā¸Ģā¸Ĩ⏞ā¸ĸ⏪⏞ā¸ĸ⏁⏞⏪ + ā¸šā¸—āš€ā¸Ŗā¸ĩā¸ĸ⏙ā¸Ēā¸´āš‰ā¸™ā¸Ēā¸¸ā¸”āšā¸Ĩāš‰ā¸§ āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ēāšˆā¸‡ā¸‚āš‰ā¸­ā¸„ā¸§ā¸˛ā¸Ąāš„ā¸”āš‰! diff --git a/libs/pandares/src/main/res/values-vi/strings.xml b/libs/pandares/src/main/res/values-vi/strings.xml index 7a865cf703..ed6cec99d9 100644 --- a/libs/pandares/src/main/res/values-vi/strings.xml +++ b/libs/pandares/src/main/res/values-vi/strings.xml @@ -1042,6 +1042,10 @@ TáēŖi xuáģ‘ng tháēĨt báēĄi TáēŖi xuáģ‘ng thành công + Đang táēŖi xuáģ‘ng + TáēŖi xuáģ‘ng thành công + Thông BÃĄo TáēŖi Xuáģ‘ng + Thông bÃĄo Canvas cho cÃĄc máģĨc táēŖi xuáģ‘ng đang diáģ…n ra. Thông Tin Chi Tiáēŋt Ngày Đáēŋn HáēĄn Đáē§y Đáģ§ Náģ™p Táē­p Tin Đính Kèm @@ -2132,4 +2136,9 @@ Ghi sáģ‘ ngày tráģ… TráēŖ láģi cháģ§ Ä‘áģ CÃĄc pháēŖn háģ“i báģ• sung (%d) + PháēŖn háģ“i cháģ§ Ä‘áģ Ä‘áēŋn háēĄn + CÃĄc pháēŖn háģ“i báģ• sung (%d) đáēŋn háēĄn + Điáģƒm Kiáģƒm Tra TháēŖo Luáē­n + Nhiáģu Ngày Đáēŋn HáēĄn + CÃĄc khÃŗa háģc Ä‘ÃŖ káēŋt thÃēc. Không tháģƒ gáģ­i tin nháē¯n! diff --git a/libs/pandares/src/main/res/values-zh/strings.xml b/libs/pandares/src/main/res/values-zh/strings.xml index 2e83da308c..e20db7e0cc 100644 --- a/libs/pandares/src/main/res/values-zh/strings.xml +++ b/libs/pandares/src/main/res/values-zh/strings.xml @@ -1026,6 +1026,10 @@ 下čŊŊå¤ąč´Ĩ 下čŊŊ成功 + æ­Ŗåœ¨ä¸‹čŊŊ + 下čŊŊ厌成 + 下čŊŊ通įŸĨ + Canvas“下čŊŊ中”通įŸĨ。 åŽŒæ•´įš„æˆĒæ­ĸæ—Ĩ期č¯Ļįģ†äŋĄæ¯ 提äē¤éĄš 附äģļ @@ -2095,4 +2099,9 @@ 输å…ĨåģļčŋŸå¤Šæ•° 回复ä¸ģéĸ˜ 更多回复 (%d) + 回复到期ä¸ģéĸ˜ + 更多到期回复 (%d) + 莨čŽēæŖ€æŸĨį‚š + 多ä¸Ē到期æ—Ĩ + č¯žį¨‹åˇ˛į쓿Ÿã€‚æ— æŗ•å‘é€æļˆæ¯īŧ diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index af60986709..9fee624f81 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -277,6 +277,7 @@ Based on graded assignments Show What-If Score What-If Score + Score cannot exceed maximum points %1$s/%2$s (%3$s) %1$s out of %2$s points, %3$s Inbox @@ -1043,6 +1044,10 @@ Downloading Download failed Download successful + Downloading + Download complete + Download Notifications + Canvas notifications for ongoing downloads. Full due date details Submissions @@ -2065,6 +2070,8 @@ All Assignments Status Filter Filter + Filter active + Filter inactive Post policy Late Expand @@ -2151,6 +2158,25 @@ Write days late Reply to topic Additional replies (%d) + Reply to topic due + Additional replies (%d) due Discussion Checkpoints Multiple Due Dates + Course concluded. Unable to send messages! + Open in Detail View + Immersive view + Statuses + Precise filtering + Differentiation tags + Students without Differentiation tags + Sort by + Student sortable name + Student name + Submission date + Submission status + Write score here + Scored More than / %s pts + Scored Less than / %s pts + Scored %1$s - %2$s + Multiple filters diff --git a/libs/pandautils/build.gradle b/libs/pandautils/build.gradle index 9cce7f814b..850901e036 100644 --- a/libs/pandautils/build.gradle +++ b/libs/pandautils/build.gradle @@ -19,6 +19,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.devtools.ksp' apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'org.jetbrains.kotlin.plugin.compose' @@ -48,6 +49,9 @@ android { ) } } + ksp { + arg("room.schemaLocation", "$projectDir/schemas".toString()) + } } buildTypes { @@ -116,6 +120,7 @@ configurations { androidTestImplementation.exclude module:'protobuf-lite' all*.resolutionStrategy { + force 'io.grpc:grpc-netty:1.75.0' force Libs.KOTLIN_STD_LIB } } @@ -132,6 +137,9 @@ dependencies { implementation Libs.LOTTIE + // Explicitly add secure grpc-netty version to override any transitive dependencies + implementation 'io.grpc:grpc-netty:1.75.0' + /* Kotlin */ implementation Libs.KOTLIN_STD_LIB implementation Libs.ANDROIDX_BROWSER @@ -197,9 +205,9 @@ dependencies { /* DI */ implementation Libs.HILT - kapt Libs.HILT_COMPILER + ksp Libs.HILT_COMPILER implementation Libs.HILT_ANDROIDX_WORK - kapt Libs.HILT_ANDROIDX_COMPILER + ksp Libs.HILT_ANDROIDX_COMPILER /* AAC */ implementation Libs.VIEW_MODEL @@ -210,7 +218,7 @@ dependencies { /* ROOM */ implementation Libs.ROOM - kapt Libs.ROOM_COMPILER + ksp Libs.ROOM_COMPILER implementation Libs.ROOM_COROUTINES /* Compose */ @@ -245,8 +253,6 @@ dependencies { // More details here: classpath https://github.com/google/ExoPlayer/issues/7905 implementation 'com.google.guava:guava:29.0-android' - kaptTest Libs.ANDROIDX_DATABINDING_COMPILER - androidTestImplementation Libs.KOTLIN_COROUTINES_TEST androidTestImplementation (project(':espresso')) { exclude group: 'org.checkerframework', module: 'checker' diff --git a/libs/pandautils/flank.yml b/libs/pandautils/flank.yml index 60bd72123a..d11ee0d065 100644 --- a/libs/pandautils/flank.yml +++ b/libs/pandautils/flank.yml @@ -8,7 +8,7 @@ gcloud: performance-metrics: false timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - notAnnotation com.instructure.canvas.espresso.annotations.E2E, com.instructure.canvas.espresso.annotations.Stub, com.instructure.canvas.espresso.annotations.FlakyE2E, com.instructure.canvas.espresso.annotations.KnownBug device: - model: Pixel2.arm version: 29 diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/13.json b/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/13.json new file mode 100644 index 0000000000..37a4c85508 --- /dev/null +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/13.json @@ -0,0 +1,722 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "e8a50c8d4caed97be61826c69921684e", + "entities": [ + { + "tableName": "AttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contentType` TEXT, `filename` TEXT, `displayName` TEXT, `url` TEXT, `thumbnailUrl` TEXT, `previewUrl` TEXT, `createdAt` INTEGER, `size` INTEGER NOT NULL, `workerId` TEXT, `submissionCommentId` INTEGER, `submissionId` INTEGER, `attempt` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionCommentId", + "columnName": "submissionCommentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AuthorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, `pronouns` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "EnvironmentFeatureFlags", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `featureFlags` TEXT NOT NULL, PRIMARY KEY(`userId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "featureFlags", + "columnName": "featureFlags", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FileUploadInputEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`workerId` TEXT NOT NULL, `courseId` INTEGER, `assignmentId` INTEGER, `quizId` INTEGER, `quizQuestionId` INTEGER, `position` INTEGER, `parentFolderId` INTEGER, `action` TEXT NOT NULL, `userId` INTEGER, `attachments` TEXT NOT NULL, `submissionId` INTEGER, `filePaths` TEXT NOT NULL, `attemptId` INTEGER, `notificationId` INTEGER, PRIMARY KEY(`workerId`))", + "fields": [ + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quizId", + "columnName": "quizId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quizQuestionId", + "columnName": "quizQuestionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "parentFolderId", + "columnName": "parentFolderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filePaths", + "columnName": "filePaths", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "workerId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MediaCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mediaId` TEXT NOT NULL, `displayName` TEXT, `url` TEXT, `mediaType` TEXT, `contentType` TEXT, PRIMARY KEY(`mediaId`))", + "fields": [ + { + "fieldPath": "mediaId", + "columnName": "mediaId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaType", + "columnName": "mediaType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "mediaId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `authorId` INTEGER NOT NULL, `authorName` TEXT, `authorPronouns` TEXT, `comment` TEXT, `createdAt` INTEGER, `mediaCommentId` TEXT, `attemptId` INTEGER, `submissionId` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorName", + "columnName": "authorName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorPronouns", + "columnName": "authorPronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "PendingSubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `pageId` TEXT NOT NULL, `comment` TEXT, `date` INTEGER NOT NULL, `status` TEXT NOT NULL, `workerId` TEXT, `filePath` TEXT, `attemptId` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filePath", + "columnName": "filePath", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DashboardFileUploadEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`workerId` TEXT NOT NULL, `userId` INTEGER NOT NULL, `title` TEXT, `subtitle` TEXT, `courseId` INTEGER, `assignmentId` INTEGER, `attemptId` INTEGER, `folderId` INTEGER, PRIMARY KEY(`workerId`))", + "fields": [ + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subtitle", + "columnName": "subtitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "workerId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReminderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `htmlUrl` TEXT NOT NULL, `name` TEXT NOT NULL, `text` TEXT NOT NULL, `time` INTEGER NOT NULL, `tag` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ModuleBulkProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`progressId` INTEGER NOT NULL, `allModules` INTEGER NOT NULL, `skipContentTags` INTEGER NOT NULL, `action` TEXT NOT NULL, `courseId` INTEGER NOT NULL, `affectedIds` TEXT NOT NULL, PRIMARY KEY(`progressId`))", + "fields": [ + { + "fieldPath": "progressId", + "columnName": "progressId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allModules", + "columnName": "allModules", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "skipContentTags", + "columnName": "skipContentTags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "affectedIds", + "columnName": "affectedIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "progressId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "assignment_filter", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userDomain` TEXT NOT NULL, `userId` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `selectedAssignmentFilters` TEXT NOT NULL, `selectedAssignmentStatusFilter` TEXT, `selectedGroupByOption` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userDomain", + "columnName": "userDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectedAssignmentFilters", + "columnName": "selectedAssignmentFilters", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "selectedAssignmentStatusFilter", + "columnName": "selectedAssignmentStatusFilter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "selectedGroupByOption", + "columnName": "selectedGroupByOption", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FileDownloadProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`workerId` TEXT NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `filePath` TEXT NOT NULL, PRIMARY KEY(`workerId`))", + "fields": [ + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "filePath", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "workerId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e8a50c8d4caed97be61826c69921684e')" + ] + } +} \ No newline at end of file diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json new file mode 100644 index 0000000000..8bfb3ebb8f --- /dev/null +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/6.json @@ -0,0 +1,5832 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "e0e8981a53e92176b25c0fb1066137d6", + "entities": [ + { + "tableName": "AssignmentDueDateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `assignmentOverrideId` INTEGER, `dueAt` TEXT, `title` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `isBase` INTEGER NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isBase", + "columnName": "isBase", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `submissionTypesRaw` TEXT NOT NULL, `dueAt` TEXT, `pointsPossible` REAL NOT NULL, `courseId` INTEGER NOT NULL, `isGradeGroupsIndividually` INTEGER NOT NULL, `gradingType` TEXT, `needsGradingCount` INTEGER NOT NULL, `htmlUrl` TEXT, `url` TEXT, `quizId` INTEGER NOT NULL, `isUseRubricForGrading` INTEGER NOT NULL, `rubricSettingsId` INTEGER, `allowedExtensions` TEXT NOT NULL, `submissionId` INTEGER, `assignmentGroupId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `isPeerReviews` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, `lockExplanation` TEXT, `discussionTopicHeaderId` INTEGER, `freeFormCriterionComments` INTEGER NOT NULL, `published` INTEGER NOT NULL, `groupCategoryId` INTEGER NOT NULL, `userSubmitted` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `onlyVisibleToOverrides` INTEGER NOT NULL, `anonymousPeerReviews` INTEGER NOT NULL, `moderatedGrading` INTEGER NOT NULL, `anonymousGrading` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `plannerOverrideId` INTEGER, `isStudioEnabled` INTEGER NOT NULL, `inClosedGradingPeriod` INTEGER NOT NULL, `annotatableAttachmentId` INTEGER NOT NULL, `anonymousSubmissions` INTEGER NOT NULL, `omitFromFinalGrade` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentGroupId`) REFERENCES `AssignmentGroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionTypesRaw", + "columnName": "submissionTypesRaw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeGroupsIndividually", + "columnName": "isGradeGroupsIndividually", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingType", + "columnName": "gradingType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizId", + "columnName": "quizId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUseRubricForGrading", + "columnName": "isUseRubricForGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricSettingsId", + "columnName": "rubricSettingsId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "allowedExtensions", + "columnName": "allowedExtensions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPeerReviews", + "columnName": "isPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userSubmitted", + "columnName": "userSubmitted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousPeerReviews", + "columnName": "anonymousPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moderatedGrading", + "columnName": "moderatedGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousGrading", + "columnName": "anonymousGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannerOverrideId", + "columnName": "plannerOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isStudioEnabled", + "columnName": "isStudioEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inClosedGradingPeriod", + "columnName": "inClosedGradingPeriod", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "annotatableAttachmentId", + "columnName": "annotatableAttachmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousSubmissions", + "columnName": "anonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "omitFromFinalGrade", + "columnName": "omitFromFinalGrade", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentGroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentGroupId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentGroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `position` INTEGER NOT NULL, `groupWeight` REAL NOT NULL, `rules` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupWeight", + "columnName": "groupWeight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rules", + "columnName": "rules", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `title` TEXT, `dueAt` INTEGER, `isAllDay` INTEGER NOT NULL, `allDayDate` TEXT, `unlockAt` INTEGER, `lockAt` INTEGER, `courseSectionId` INTEGER NOT NULL, `groupId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayDate", + "columnName": "allDayDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentRubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `rubricId` TEXT NOT NULL, PRIMARY KEY(`assignmentId`, `rubricId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricId", + "columnName": "rubricId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId", + "rubricId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentScoreStatisticsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `mean` REAL NOT NULL, `min` REAL NOT NULL, `max` REAL NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mean", + "columnName": "mean", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "min", + "columnName": "min", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "max", + "columnName": "max", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentSetEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `scoringRangeId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `position` INTEGER NOT NULL, `masteryPathId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`masteryPathId`) REFERENCES `MasteryPathEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringRangeId", + "columnName": "scoringRangeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "masteryPathId", + "columnName": "masteryPathId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "MasteryPathEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "masteryPathId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `originalName` TEXT, `courseCode` TEXT, `startAt` TEXT, `endAt` TEXT, `syllabusBody` TEXT, `hideFinalGrades` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `license` TEXT NOT NULL, `termId` INTEGER, `needsGradingCount` INTEGER NOT NULL, `isApplyAssignmentGroupWeights` INTEGER NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, `isFavorite` INTEGER NOT NULL, `accessRestrictedByDate` INTEGER NOT NULL, `imageUrl` TEXT, `bannerImageUrl` TEXT, `isWeightedGradingPeriods` INTEGER NOT NULL, `hasGradingPeriods` INTEGER NOT NULL, `homePage` TEXT, `restrictEnrollmentsToCourseDate` INTEGER NOT NULL, `workflowState` TEXT, `homeroomCourse` INTEGER NOT NULL, `courseColor` TEXT, `gradingScheme` TEXT, `pointsBasedGradingScheme` INTEGER NOT NULL, `scalingFactor` REAL NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`termId`) REFERENCES `TermEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syllabusBody", + "columnName": "syllabusBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideFinalGrades", + "columnName": "hideFinalGrades", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "license", + "columnName": "license", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "termId", + "columnName": "termId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isApplyAssignmentGroupWeights", + "columnName": "isApplyAssignmentGroupWeights", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessRestrictedByDate", + "columnName": "accessRestrictedByDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imageUrl", + "columnName": "imageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bannerImageUrl", + "columnName": "bannerImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isWeightedGradingPeriods", + "columnName": "isWeightedGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasGradingPeriods", + "columnName": "hasGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homePage", + "columnName": "homePage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "restrictEnrollmentsToCourseDate", + "columnName": "restrictEnrollmentsToCourseDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homeroomCourse", + "columnName": "homeroomCourse", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseColor", + "columnName": "courseColor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "gradingScheme", + "columnName": "gradingScheme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsBasedGradingScheme", + "columnName": "pointsBasedGradingScheme", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scalingFactor", + "columnName": "scalingFactor", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "TermEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "termId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFilesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`courseId`, `url`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "url" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "CourseGradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `gradingPeriodId` INTEGER NOT NULL, PRIMARY KEY(`courseId`, `gradingPeriodId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`gradingPeriodId`) REFERENCES `GradingPeriodEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "gradingPeriodId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "GradingPeriodEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "gradingPeriodId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseSummary` INTEGER, `restrictQuantitativeData` INTEGER NOT NULL, PRIMARY KEY(`courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseSummary", + "columnName": "courseSummary", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "restrictQuantitativeData", + "columnName": "restrictQuantitativeData", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `fullContentSync` INTEGER NOT NULL, `tabs` TEXT NOT NULL, `fullFileSync` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullContentSync", + "columnName": "fullContentSync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullFileSync", + "columnName": "fullFileSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DashboardCardEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isK5Subject` INTEGER NOT NULL, `shortName` TEXT, `originalName` TEXT, `courseCode` TEXT, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isK5Subject", + "columnName": "isK5Subject", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionEntryAttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionEntryId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionEntryId`, `remoteFileId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionEntryId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `updatedAt` TEXT, `createdAt` TEXT, `authorId` INTEGER, `description` TEXT, `userId` INTEGER NOT NULL, `parentId` INTEGER NOT NULL, `message` TEXT, `deleted` INTEGER NOT NULL, `totalChildren` INTEGER NOT NULL, `unreadChildren` INTEGER NOT NULL, `ratingCount` INTEGER NOT NULL, `ratingSum` INTEGER NOT NULL, `editorId` INTEGER NOT NULL, `_hasRated` INTEGER NOT NULL, `replyIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChildren", + "columnName": "totalChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadChildren", + "columnName": "unreadChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingCount", + "columnName": "ratingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingSum", + "columnName": "ratingSum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editorId", + "columnName": "editorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "_hasRated", + "columnName": "_hasRated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyIds", + "columnName": "replyIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionParticipantEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `pronouns` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionTopicHeaderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `discussionType` TEXT, `title` TEXT, `message` TEXT, `htmlUrl` TEXT, `postedDate` INTEGER, `delayedPostDate` INTEGER, `lastReplyDate` INTEGER, `requireInitialPost` INTEGER NOT NULL, `discussionSubentryCount` INTEGER NOT NULL, `readState` TEXT, `unreadCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `assignmentId` INTEGER, `locked` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `pinned` INTEGER NOT NULL, `authorId` INTEGER, `podcastUrl` TEXT, `groupCategoryId` TEXT, `announcement` INTEGER NOT NULL, `permissionId` INTEGER, `published` INTEGER NOT NULL, `allowRating` INTEGER NOT NULL, `onlyGradersCanRate` INTEGER NOT NULL, `sortByRating` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `lockAt` INTEGER, `userCanSeePosts` INTEGER NOT NULL, `specificSections` TEXT, `anonymousState` TEXT, `replyRequiredCount` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`authorId`) REFERENCES `DiscussionParticipantEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`permissionId`) REFERENCES `DiscussionTopicPermissionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionType", + "columnName": "discussionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedDate", + "columnName": "postedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "delayedPostDate", + "columnName": "delayedPostDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastReplyDate", + "columnName": "lastReplyDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "requireInitialPost", + "columnName": "requireInitialPost", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionSubentryCount", + "columnName": "discussionSubentryCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readState", + "columnName": "readState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unreadCount", + "columnName": "unreadCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "podcastUrl", + "columnName": "podcastUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "announcement", + "columnName": "announcement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "permissionId", + "columnName": "permissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowRating", + "columnName": "allowRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyGradersCanRate", + "columnName": "onlyGradersCanRate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortByRating", + "columnName": "sortByRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userCanSeePosts", + "columnName": "userCanSeePosts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "specificSections", + "columnName": "specificSections", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "anonymousState", + "columnName": "anonymousState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replyRequiredCount", + "columnName": "replyRequiredCount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionParticipantEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "authorId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "DiscussionTopicPermissionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "permissionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicPermissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `discussionTopicHeaderId` INTEGER NOT NULL, `attach` INTEGER NOT NULL, `update` INTEGER NOT NULL, `delete` INTEGER NOT NULL, `reply` INTEGER NOT NULL, FOREIGN KEY(`discussionTopicHeaderId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attach", + "columnName": "attach", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delete", + "columnName": "delete", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reply", + "columnName": "reply", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicHeaderId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicRemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionId`, `remoteFileId`), FOREIGN KEY(`discussionId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionId", + "columnName": "discussionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicSectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionTopicId` INTEGER NOT NULL, `sectionId` INTEGER NOT NULL, PRIMARY KEY(`discussionTopicId`, `sectionId`), FOREIGN KEY(`discussionTopicId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionTopicId", + "columnName": "discussionTopicId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionTopicId", + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "EnrollmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `role` TEXT NOT NULL, `type` TEXT NOT NULL, `courseId` INTEGER, `courseSectionId` INTEGER, `enrollmentState` TEXT, `userId` INTEGER NOT NULL, `computedCurrentScore` REAL, `computedFinalScore` REAL, `computedCurrentGrade` TEXT, `computedFinalGrade` TEXT, `multipleGradingPeriodsEnabled` INTEGER NOT NULL, `totalsForAllGradingPeriodsOption` INTEGER NOT NULL, `currentPeriodComputedCurrentScore` REAL, `currentPeriodComputedFinalScore` REAL, `currentPeriodComputedCurrentGrade` TEXT, `currentPeriodComputedFinalGrade` TEXT, `currentGradingPeriodId` INTEGER NOT NULL, `currentGradingPeriodTitle` TEXT, `associatedUserId` INTEGER NOT NULL, `lastActivityAt` INTEGER, `limitPrivilegesToCourseSection` INTEGER NOT NULL, `observedUserId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`observedUserId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseSectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "computedCurrentScore", + "columnName": "computedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedFinalScore", + "columnName": "computedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedCurrentGrade", + "columnName": "computedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "computedFinalGrade", + "columnName": "computedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "multipleGradingPeriodsEnabled", + "columnName": "multipleGradingPeriodsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalsForAllGradingPeriodsOption", + "columnName": "totalsForAllGradingPeriodsOption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPeriodComputedCurrentScore", + "columnName": "currentPeriodComputedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalScore", + "columnName": "currentPeriodComputedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedCurrentGrade", + "columnName": "currentPeriodComputedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalGrade", + "columnName": "currentPeriodComputedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentGradingPeriodId", + "columnName": "currentGradingPeriodId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentGradingPeriodTitle", + "columnName": "currentGradingPeriodTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "associatedUserId", + "columnName": "associatedUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivityAt", + "columnName": "lastActivityAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "limitPrivilegesToCourseSection", + "columnName": "limitPrivilegesToCourseSection", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "observedUserId", + "columnName": "observedUserId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "observedUserId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "courseSectionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileFolderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `createdDate` INTEGER, `updatedDate` INTEGER, `unlockDate` INTEGER, `lockDate` INTEGER, `isLocked` INTEGER NOT NULL, `isHidden` INTEGER NOT NULL, `isLockedForUser` INTEGER NOT NULL, `isHiddenForUser` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `size` INTEGER NOT NULL, `contentType` TEXT, `url` TEXT, `displayName` TEXT, `thumbnailUrl` TEXT, `parentFolderId` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `filesCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `foldersCount` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `foldersUrl` TEXT, `filesUrl` TEXT, `fullName` TEXT, `forSubmissions` INTEGER NOT NULL, `canUpload` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedDate", + "columnName": "updatedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unlockDate", + "columnName": "unlockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockDate", + "columnName": "lockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLockedForUser", + "columnName": "isLockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHiddenForUser", + "columnName": "isHiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentFolderId", + "columnName": "parentFolderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filesCount", + "columnName": "filesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "foldersCount", + "columnName": "foldersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "foldersUrl", + "columnName": "foldersUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesUrl", + "columnName": "filesUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "forSubmissions", + "columnName": "forSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canUpload", + "columnName": "canUpload", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "EditDashboardItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `name` TEXT NOT NULL, `isFavorite` INTEGER NOT NULL, `enrollmentState` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ExternalToolAttributesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `url` TEXT, `newTab` INTEGER NOT NULL, `resourceLinkid` TEXT, `contentId` INTEGER, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "newTab", + "columnName": "newTab", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceLinkid", + "columnName": "resourceLinkid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`enrollmentId` INTEGER NOT NULL, `htmlUrl` TEXT NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, PRIMARY KEY(`enrollmentId`), FOREIGN KEY(`enrollmentId`) REFERENCES `EnrollmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "enrollmentId", + "columnName": "enrollmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "enrollmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "EnrollmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "enrollmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `startDate` TEXT, `endDate` TEXT, `weight` REAL NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endDate", + "columnName": "endDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `avatarUrl` TEXT, `isPublic` INTEGER NOT NULL, `membersCount` INTEGER NOT NULL, `joinLevel` TEXT, `courseId` INTEGER NOT NULL, `accountId` INTEGER NOT NULL, `role` TEXT, `groupCategoryId` INTEGER NOT NULL, `storageQuotaMb` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `concluded` INTEGER NOT NULL, `canAccess` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "membersCount", + "columnName": "membersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinLevel", + "columnName": "joinLevel", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "storageQuotaMb", + "columnName": "storageQuotaMb", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concluded", + "columnName": "concluded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canAccess", + "columnName": "canAccess", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupUserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LocalFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `createdDate` INTEGER NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MasteryPathAssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `overrideId` INTEGER NOT NULL, `assignmentSetId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentSetId`) REFERENCES `AssignmentSetEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "overrideId", + "columnName": "overrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentSetId", + "columnName": "assignmentSetId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentSetEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentSetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MasteryPathEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `selectedSetId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectedSetId", + "columnName": "selectedSetId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleContentDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `pointsPossible` TEXT, `dueAt` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `moduleId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `title` TEXT, `indent` INTEGER NOT NULL, `type` TEXT, `htmlUrl` TEXT, `url` TEXT, `published` INTEGER, `contentId` INTEGER NOT NULL, `externalUrl` TEXT, `pageUrl` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`moduleId`) REFERENCES `ModuleObjectEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "indent", + "columnName": "indent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageUrl", + "columnName": "pageUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleObjectEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleObjectEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `name` TEXT, `unlockAt` TEXT, `sequentialProgress` INTEGER NOT NULL, `prerequisiteIds` TEXT, `state` TEXT, `completedAt` TEXT, `published` INTEGER, `itemCount` INTEGER NOT NULL, `itemsUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sequentialProgress", + "columnName": "sequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prerequisiteIds", + "columnName": "prerequisiteIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "completedAt", + "columnName": "completedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemCount", + "columnName": "itemCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemsUrl", + "columnName": "itemsUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "NeedsGradingCountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sectionId` INTEGER NOT NULL, `needsGradingCount` INTEGER NOT NULL, PRIMARY KEY(`sectionId`), FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `createdAt` INTEGER, `updatedAt` INTEGER, `hideFromStudents` INTEGER NOT NULL, `status` TEXT, `body` TEXT, `frontPage` INTEGER NOT NULL, `published` INTEGER NOT NULL, `editingRoles` TEXT, `htmlUrl` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hideFromStudents", + "columnName": "hideFromStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "frontPage", + "columnName": "frontPage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editingRoles", + "columnName": "editingRoles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PlannerOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `plannableType` TEXT NOT NULL, `plannableId` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, `markedComplete` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannableType", + "columnName": "plannableType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "plannableId", + "columnName": "plannableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "markedComplete", + "columnName": "markedComplete", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `displayName` TEXT, `fileName` TEXT, `contentType` TEXT, `url` TEXT, `size` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `unlockAt` TEXT, `locked` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `lockAt` TEXT, `hiddenForUser` INTEGER NOT NULL, `thumbnailUrl` TEXT, `modifiedAt` TEXT, `lockedForUser` INTEGER NOT NULL, `previewUrl` TEXT, `lockExplanation` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hiddenForUser", + "columnName": "hiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "modifiedAt", + "columnName": "modifiedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RubricCriterionAssessmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `assignmentId` INTEGER NOT NULL, `ratingId` TEXT, `points` REAL, `comments` TEXT, PRIMARY KEY(`id`, `assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingId", + "columnName": "ratingId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "comments", + "columnName": "comments", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `criterionUseRange` INTEGER NOT NULL, `ignoreForScoring` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "criterionUseRange", + "columnName": "criterionUseRange", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ignoreForScoring", + "columnName": "ignoreForScoring", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionRatingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `rubricCriterionId` TEXT NOT NULL, PRIMARY KEY(`id`, `rubricCriterionId`), FOREIGN KEY(`rubricCriterionId`) REFERENCES `RubricCriterionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rubricCriterionId", + "columnName": "rubricCriterionId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "rubricCriterionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "RubricCriterionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "rubricCriterionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `pointsPossible` REAL NOT NULL, `title` TEXT NOT NULL, `isReusable` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `isReadOnly` INTEGER NOT NULL, `freeFormCriterionComments` INTEGER NOT NULL, `hideScoreTotal` INTEGER NOT NULL, `hidePoints` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isReusable", + "columnName": "isReusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isReadOnly", + "columnName": "isReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hideScoreTotal", + "columnName": "hideScoreTotal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidePoints", + "columnName": "hidePoints", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemAssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentOverrideId` INTEGER NOT NULL, `scheduleItemId` TEXT NOT NULL, PRIMARY KEY(`assignmentOverrideId`, `scheduleItemId`), FOREIGN KEY(`assignmentOverrideId`) REFERENCES `AssignmentOverrideEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`scheduleItemId`) REFERENCES `ScheduleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduleItemId", + "columnName": "scheduleItemId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentOverrideId", + "scheduleItemId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentOverrideEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentOverrideId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "ScheduleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "scheduleItemId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `description` TEXT, `startAt` TEXT, `endAt` TEXT, `isAllDay` INTEGER NOT NULL, `allDayAt` TEXT, `locationAddress` TEXT, `locationName` TEXT, `htmlUrl` TEXT, `contextCode` TEXT, `effectiveContextCode` TEXT, `isHidden` INTEGER NOT NULL, `importantDates` INTEGER NOT NULL, `assignmentId` INTEGER, `type` TEXT NOT NULL, `itemType` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayAt", + "columnName": "allDayAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationAddress", + "columnName": "locationAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationName", + "columnName": "locationName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextCode", + "columnName": "contextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effectiveContextCode", + "columnName": "effectiveContextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "importantDates", + "columnName": "importantDates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemType", + "columnName": "itemType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER, `startAt` TEXT, `endAt` TEXT, `totalStudents` INTEGER NOT NULL, `restrictEnrollmentsToSectionDates` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "totalStudents", + "columnName": "totalStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "restrictEnrollmentsToSectionDates", + "columnName": "restrictEnrollmentsToSectionDates", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionDiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`submissionId` INTEGER NOT NULL, `discussionEntryId` INTEGER NOT NULL, PRIMARY KEY(`submissionId`, `discussionEntryId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "submissionId", + "discussionEntryId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `attempt` INTEGER NOT NULL, `submittedAt` INTEGER, `commentCreated` INTEGER, `mediaContentType` TEXT, `mediaCommentUrl` TEXT, `mediaCommentDisplay` TEXT, `body` TEXT, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, `workflowState` TEXT, `submissionType` TEXT, `previewUrl` TEXT, `url` TEXT, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `mediaCommentId` TEXT, `assignmentId` INTEGER NOT NULL, `userId` INTEGER, `graderId` INTEGER, `groupId` INTEGER, `pointsDeducted` REAL, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `postedAt` INTEGER, `gradingPeriodId` INTEGER, `customGradeStatusId` INTEGER, `hasSubAssignmentSubmissions` INTEGER NOT NULL, PRIMARY KEY(`id`, `attempt`), FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submittedAt", + "columnName": "submittedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "commentCreated", + "columnName": "commentCreated", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaContentType", + "columnName": "mediaContentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentUrl", + "columnName": "mediaCommentUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentDisplay", + "columnName": "mediaCommentDisplay", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionType", + "columnName": "submissionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "graderId", + "columnName": "graderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pointsDeducted", + "columnName": "pointsDeducted", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedAt", + "columnName": "postedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasSubAssignmentSubmissions", + "columnName": "hasSubAssignmentSubmissions", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "attempt" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `autoSyncEnabled` INTEGER NOT NULL, `syncFrequency` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoSyncEnabled", + "columnName": "autoSyncEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncFrequency", + "columnName": "syncFrequency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifiOnly", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TabEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `label` TEXT, `type` TEXT NOT NULL, `htmlUrl` TEXT, `externalUrl` TEXT, `visibility` TEXT NOT NULL, `isHidden` INTEGER NOT NULL, `position` INTEGER NOT NULL, `ltiUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ltiUrl", + "columnName": "ltiUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "TermEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `startAt` TEXT, `endAt` TEXT, `isGroupTerm` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGroupTerm", + "columnName": "isGroupTerm", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserCalendarEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ics` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ics", + "columnName": "ics", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `shortName` TEXT, `loginId` TEXT, `avatarUrl` TEXT, `primaryEmail` TEXT, `email` TEXT, `sortableName` TEXT, `bio` TEXT, `enrollmentIndex` INTEGER NOT NULL, `lastLogin` TEXT, `locale` TEXT, `effective_locale` TEXT, `pronouns` TEXT, `k5User` INTEGER NOT NULL, `rootAccount` TEXT, `isFakeStudent` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginId", + "columnName": "loginId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "primaryEmail", + "columnName": "primaryEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortableName", + "columnName": "sortableName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bio", + "columnName": "bio", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enrollmentIndex", + "columnName": "enrollmentIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastLogin", + "columnName": "lastLogin", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effective_locale", + "columnName": "effective_locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "k5User", + "columnName": "k5User", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rootAccount", + "columnName": "rootAccount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFakeStudent", + "columnName": "isFakeStudent", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "QuizEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `mobileUrl` TEXT, `htmlUrl` TEXT, `description` TEXT, `quizType` TEXT, `assignmentGroupId` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `questionCount` INTEGER NOT NULL, `pointsPossible` TEXT, `isLockQuestionsAfterAnswering` INTEGER NOT NULL, `dueAt` TEXT, `timeLimit` INTEGER NOT NULL, `shuffleAnswers` INTEGER NOT NULL, `showCorrectAnswers` INTEGER NOT NULL, `scoringPolicy` TEXT, `accessCode` TEXT, `ipFilter` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `hideResults` TEXT, `showCorrectAnswersAt` TEXT, `hideCorrectAnswersAt` TEXT, `unlockAt` TEXT, `oneTimeResults` INTEGER NOT NULL, `lockAt` TEXT, `questionTypes` TEXT NOT NULL, `hasAccessCode` INTEGER NOT NULL, `oneQuestionAtATime` INTEGER NOT NULL, `requireLockdownBrowser` INTEGER NOT NULL, `requireLockdownBrowserForResults` INTEGER NOT NULL, `allowAnonymousSubmissions` INTEGER NOT NULL, `published` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `isOnlyVisibleToOverrides` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mobileUrl", + "columnName": "mobileUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizType", + "columnName": "quizType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionCount", + "columnName": "questionCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isLockQuestionsAfterAnswering", + "columnName": "isLockQuestionsAfterAnswering", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeLimit", + "columnName": "timeLimit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shuffleAnswers", + "columnName": "shuffleAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showCorrectAnswers", + "columnName": "showCorrectAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringPolicy", + "columnName": "scoringPolicy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accessCode", + "columnName": "accessCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ipFilter", + "columnName": "ipFilter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideResults", + "columnName": "hideResults", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "showCorrectAnswersAt", + "columnName": "showCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideCorrectAnswersAt", + "columnName": "hideCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oneTimeResults", + "columnName": "oneTimeResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "questionTypes", + "columnName": "questionTypes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasAccessCode", + "columnName": "hasAccessCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneQuestionAtATime", + "columnName": "oneQuestionAtATime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowser", + "columnName": "requireLockdownBrowser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowserForResults", + "columnName": "requireLockdownBrowserForResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowAnonymousSubmissions", + "columnName": "allowAnonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isOnlyVisibleToOverrides", + "columnName": "isOnlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockInfoEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `modulePrerequisiteNames` TEXT, `unlockAt` TEXT, `lockedModuleId` INTEGER, `assignmentId` INTEGER, `moduleId` INTEGER, `pageId` INTEGER, FOREIGN KEY(`moduleId`) REFERENCES `ModuleContentDetailsEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`pageId`) REFERENCES `PageEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "modulePrerequisiteNames", + "columnName": "modulePrerequisiteNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleContentDetailsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "PageEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pageId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockedModuleEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `unlockAt` TEXT, `isRequireSequentialProgress` INTEGER NOT NULL, `lockInfoId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`lockInfoId`) REFERENCES `LockInfoEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isRequireSequentialProgress", + "columnName": "isRequireSequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockInfoId", + "columnName": "lockInfoId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockInfoEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockInfoId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleNameEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `lockedModuleId` INTEGER NOT NULL, FOREIGN KEY(`lockedModuleId`) REFERENCES `LockedModuleEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockedModuleEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockedModuleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleCompletionRequirementEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT, `minScore` REAL NOT NULL, `maxScore` REAL NOT NULL, `completed` INTEGER, `moduleId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "minScore", + "columnName": "minScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxScore", + "columnName": "maxScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `fileName` TEXT, `courseId` INTEGER NOT NULL, `url` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "ConferenceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `conferenceKey` TEXT, `conferenceType` TEXT, `description` TEXT, `duration` INTEGER NOT NULL, `endedAt` INTEGER, `hasAdvancedSettings` INTEGER NOT NULL, `joinUrl` TEXT, `longRunning` INTEGER NOT NULL, `startedAt` INTEGER, `title` TEXT, `url` TEXT, `contextType` TEXT NOT NULL, `contextId` INTEGER NOT NULL, `record` INTEGER, `users` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conferenceKey", + "columnName": "conferenceKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "conferenceType", + "columnName": "conferenceType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAdvancedSettings", + "columnName": "hasAdvancedSettings", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinUrl", + "columnName": "joinUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longRunning", + "columnName": "longRunning", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startedAt", + "columnName": "startedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "record", + "columnName": "record", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "users", + "columnName": "users", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ConferenceRecordingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`recordingId` TEXT NOT NULL, `conferenceId` INTEGER NOT NULL, `createdAtMillis` INTEGER NOT NULL, `durationMinutes` INTEGER NOT NULL, `playbackUrl` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`recordingId`), FOREIGN KEY(`conferenceId`) REFERENCES `ConferenceEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "recordingId", + "columnName": "recordingId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conferenceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAtMillis", + "columnName": "createdAtMillis", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durationMinutes", + "columnName": "durationMinutes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playbackUrl", + "columnName": "playbackUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "recordingId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ConferenceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "conferenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFeaturesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `features` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contentType` TEXT, `filename` TEXT, `displayName` TEXT, `url` TEXT, `thumbnailUrl` TEXT, `previewUrl` TEXT, `createdAt` INTEGER, `size` INTEGER NOT NULL, `workerId` TEXT, `submissionCommentId` INTEGER, `submissionId` INTEGER, `attempt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionCommentId`) REFERENCES `SubmissionCommentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionCommentId", + "columnName": "submissionCommentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionCommentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionCommentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MediaCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mediaId` TEXT NOT NULL, `submissionId` INTEGER NOT NULL, `attemptId` INTEGER NOT NULL, `displayName` TEXT, `url` TEXT, `mediaType` TEXT, `contentType` TEXT, PRIMARY KEY(`mediaId`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "mediaId", + "columnName": "mediaId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaType", + "columnName": "mediaType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "mediaId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "AuthorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, `pronouns` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `authorId` INTEGER NOT NULL, `authorName` TEXT, `authorPronouns` TEXT, `comment` TEXT, `createdAt` INTEGER, `mediaCommentId` TEXT, `attemptId` INTEGER, `submissionId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorName", + "columnName": "authorName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorPronouns", + "columnName": "authorPronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "DiscussionTopicEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unreadEntries` TEXT NOT NULL, `participantIds` TEXT NOT NULL, `viewIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadEntries", + "columnName": "unreadEntries", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantIds", + "columnName": "participantIds", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viewIds", + "columnName": "viewIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CourseSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `tabs` TEXT NOT NULL, `additionalFilesStarted` INTEGER NOT NULL, `progressState` TEXT NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "additionalFilesStarted", + "columnName": "additionalFilesStarted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FileSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `additionalFile` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseSyncProgressEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "fileId", + "columnName": "fileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "additionalFile", + "columnName": "additionalFile", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncProgressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "StudioMediaProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ltiLaunchId` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "ltiLaunchId", + "columnName": "ltiLaunchId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CustomGradeStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CheckpointEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `assignmentId` INTEGER NOT NULL, `name` TEXT, `tag` TEXT, `pointsPossible` REAL, `dueAt` TEXT, `onlyVisibleToOverrides` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubAssignmentSubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `submissionId` INTEGER NOT NULL, `submissionAttempt` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `latePolicyStatus` TEXT, `customGradeStatusId` INTEGER, `subAssignmentTag` TEXT, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `userId` INTEGER NOT NULL, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, FOREIGN KEY(`submissionId`, `submissionAttempt`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionAttempt", + "columnName": "submissionAttempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latePolicyStatus", + "columnName": "latePolicyStatus", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subAssignmentTag", + "columnName": "subAssignmentTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "submissionAttempt" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e0e8981a53e92176b25c0fb1066137d6')" + ] + } +} \ No newline at end of file diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/7.json b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/7.json new file mode 100644 index 0000000000..435ec0fe7d --- /dev/null +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/7.json @@ -0,0 +1,6002 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "452f8a3a37230a66a3dafed1956528e6", + "entities": [ + { + "tableName": "AssignmentDueDateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `assignmentOverrideId` INTEGER, `dueAt` TEXT, `title` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `isBase` INTEGER NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isBase", + "columnName": "isBase", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `submissionTypesRaw` TEXT NOT NULL, `dueAt` TEXT, `pointsPossible` REAL NOT NULL, `courseId` INTEGER NOT NULL, `isGradeGroupsIndividually` INTEGER NOT NULL, `gradingType` TEXT, `needsGradingCount` INTEGER NOT NULL, `htmlUrl` TEXT, `url` TEXT, `quizId` INTEGER NOT NULL, `isUseRubricForGrading` INTEGER NOT NULL, `rubricSettingsId` INTEGER, `allowedExtensions` TEXT NOT NULL, `submissionId` INTEGER, `assignmentGroupId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `isPeerReviews` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, `lockExplanation` TEXT, `discussionTopicHeaderId` INTEGER, `freeFormCriterionComments` INTEGER NOT NULL, `published` INTEGER NOT NULL, `groupCategoryId` INTEGER NOT NULL, `userSubmitted` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `onlyVisibleToOverrides` INTEGER NOT NULL, `anonymousPeerReviews` INTEGER NOT NULL, `moderatedGrading` INTEGER NOT NULL, `anonymousGrading` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `plannerOverrideId` INTEGER, `isStudioEnabled` INTEGER NOT NULL, `inClosedGradingPeriod` INTEGER NOT NULL, `annotatableAttachmentId` INTEGER NOT NULL, `anonymousSubmissions` INTEGER NOT NULL, `omitFromFinalGrade` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentGroupId`) REFERENCES `AssignmentGroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionTypesRaw", + "columnName": "submissionTypesRaw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeGroupsIndividually", + "columnName": "isGradeGroupsIndividually", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingType", + "columnName": "gradingType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizId", + "columnName": "quizId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUseRubricForGrading", + "columnName": "isUseRubricForGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricSettingsId", + "columnName": "rubricSettingsId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "allowedExtensions", + "columnName": "allowedExtensions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPeerReviews", + "columnName": "isPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userSubmitted", + "columnName": "userSubmitted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousPeerReviews", + "columnName": "anonymousPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moderatedGrading", + "columnName": "moderatedGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousGrading", + "columnName": "anonymousGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannerOverrideId", + "columnName": "plannerOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isStudioEnabled", + "columnName": "isStudioEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inClosedGradingPeriod", + "columnName": "inClosedGradingPeriod", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "annotatableAttachmentId", + "columnName": "annotatableAttachmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousSubmissions", + "columnName": "anonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "omitFromFinalGrade", + "columnName": "omitFromFinalGrade", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentGroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentGroupId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentGroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `position` INTEGER NOT NULL, `groupWeight` REAL NOT NULL, `rules` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupWeight", + "columnName": "groupWeight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rules", + "columnName": "rules", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `title` TEXT, `dueAt` INTEGER, `isAllDay` INTEGER NOT NULL, `allDayDate` TEXT, `unlockAt` INTEGER, `lockAt` INTEGER, `courseSectionId` INTEGER NOT NULL, `groupId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayDate", + "columnName": "allDayDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentRubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `rubricId` TEXT NOT NULL, PRIMARY KEY(`assignmentId`, `rubricId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricId", + "columnName": "rubricId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId", + "rubricId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentScoreStatisticsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `mean` REAL NOT NULL, `min` REAL NOT NULL, `max` REAL NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mean", + "columnName": "mean", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "min", + "columnName": "min", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "max", + "columnName": "max", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentSetEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `scoringRangeId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `position` INTEGER NOT NULL, `masteryPathId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`masteryPathId`) REFERENCES `MasteryPathEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringRangeId", + "columnName": "scoringRangeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "masteryPathId", + "columnName": "masteryPathId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "MasteryPathEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "masteryPathId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `originalName` TEXT, `courseCode` TEXT, `startAt` TEXT, `endAt` TEXT, `syllabusBody` TEXT, `hideFinalGrades` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `license` TEXT NOT NULL, `termId` INTEGER, `needsGradingCount` INTEGER NOT NULL, `isApplyAssignmentGroupWeights` INTEGER NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, `isFavorite` INTEGER NOT NULL, `accessRestrictedByDate` INTEGER NOT NULL, `imageUrl` TEXT, `bannerImageUrl` TEXT, `isWeightedGradingPeriods` INTEGER NOT NULL, `hasGradingPeriods` INTEGER NOT NULL, `homePage` TEXT, `restrictEnrollmentsToCourseDate` INTEGER NOT NULL, `workflowState` TEXT, `homeroomCourse` INTEGER NOT NULL, `courseColor` TEXT, `gradingScheme` TEXT, `pointsBasedGradingScheme` INTEGER NOT NULL, `scalingFactor` REAL NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`termId`) REFERENCES `TermEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syllabusBody", + "columnName": "syllabusBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideFinalGrades", + "columnName": "hideFinalGrades", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "license", + "columnName": "license", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "termId", + "columnName": "termId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isApplyAssignmentGroupWeights", + "columnName": "isApplyAssignmentGroupWeights", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessRestrictedByDate", + "columnName": "accessRestrictedByDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imageUrl", + "columnName": "imageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bannerImageUrl", + "columnName": "bannerImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isWeightedGradingPeriods", + "columnName": "isWeightedGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasGradingPeriods", + "columnName": "hasGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homePage", + "columnName": "homePage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "restrictEnrollmentsToCourseDate", + "columnName": "restrictEnrollmentsToCourseDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homeroomCourse", + "columnName": "homeroomCourse", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseColor", + "columnName": "courseColor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "gradingScheme", + "columnName": "gradingScheme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsBasedGradingScheme", + "columnName": "pointsBasedGradingScheme", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scalingFactor", + "columnName": "scalingFactor", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "TermEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "termId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFilesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`courseId`, `url`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "url" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "CourseGradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `gradingPeriodId` INTEGER NOT NULL, PRIMARY KEY(`courseId`, `gradingPeriodId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`gradingPeriodId`) REFERENCES `GradingPeriodEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "gradingPeriodId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "GradingPeriodEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "gradingPeriodId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseSummary` INTEGER, `restrictQuantitativeData` INTEGER NOT NULL, PRIMARY KEY(`courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseSummary", + "columnName": "courseSummary", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "restrictQuantitativeData", + "columnName": "restrictQuantitativeData", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `fullContentSync` INTEGER NOT NULL, `tabs` TEXT NOT NULL, `fullFileSync` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullContentSync", + "columnName": "fullContentSync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullFileSync", + "columnName": "fullFileSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DashboardCardEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isK5Subject` INTEGER NOT NULL, `shortName` TEXT, `originalName` TEXT, `courseCode` TEXT, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isK5Subject", + "columnName": "isK5Subject", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionEntryAttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionEntryId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionEntryId`, `remoteFileId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionEntryId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `updatedAt` TEXT, `createdAt` TEXT, `authorId` INTEGER, `description` TEXT, `userId` INTEGER NOT NULL, `parentId` INTEGER NOT NULL, `message` TEXT, `deleted` INTEGER NOT NULL, `totalChildren` INTEGER NOT NULL, `unreadChildren` INTEGER NOT NULL, `ratingCount` INTEGER NOT NULL, `ratingSum` INTEGER NOT NULL, `editorId` INTEGER NOT NULL, `_hasRated` INTEGER NOT NULL, `replyIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChildren", + "columnName": "totalChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadChildren", + "columnName": "unreadChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingCount", + "columnName": "ratingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingSum", + "columnName": "ratingSum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editorId", + "columnName": "editorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "_hasRated", + "columnName": "_hasRated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyIds", + "columnName": "replyIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionParticipantEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `pronouns` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionTopicHeaderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `discussionType` TEXT, `title` TEXT, `message` TEXT, `htmlUrl` TEXT, `postedDate` INTEGER, `delayedPostDate` INTEGER, `lastReplyDate` INTEGER, `requireInitialPost` INTEGER NOT NULL, `discussionSubentryCount` INTEGER NOT NULL, `readState` TEXT, `unreadCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `assignmentId` INTEGER, `locked` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `pinned` INTEGER NOT NULL, `authorId` INTEGER, `podcastUrl` TEXT, `groupCategoryId` TEXT, `announcement` INTEGER NOT NULL, `permissionId` INTEGER, `published` INTEGER NOT NULL, `allowRating` INTEGER NOT NULL, `onlyGradersCanRate` INTEGER NOT NULL, `sortByRating` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `lockAt` INTEGER, `userCanSeePosts` INTEGER NOT NULL, `specificSections` TEXT, `anonymousState` TEXT, `replyRequiredCount` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`authorId`) REFERENCES `DiscussionParticipantEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`permissionId`) REFERENCES `DiscussionTopicPermissionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionType", + "columnName": "discussionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedDate", + "columnName": "postedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "delayedPostDate", + "columnName": "delayedPostDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastReplyDate", + "columnName": "lastReplyDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "requireInitialPost", + "columnName": "requireInitialPost", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionSubentryCount", + "columnName": "discussionSubentryCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readState", + "columnName": "readState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unreadCount", + "columnName": "unreadCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "podcastUrl", + "columnName": "podcastUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "announcement", + "columnName": "announcement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "permissionId", + "columnName": "permissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowRating", + "columnName": "allowRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyGradersCanRate", + "columnName": "onlyGradersCanRate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortByRating", + "columnName": "sortByRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userCanSeePosts", + "columnName": "userCanSeePosts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "specificSections", + "columnName": "specificSections", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "anonymousState", + "columnName": "anonymousState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replyRequiredCount", + "columnName": "replyRequiredCount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionParticipantEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "authorId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "DiscussionTopicPermissionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "permissionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicPermissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `discussionTopicHeaderId` INTEGER NOT NULL, `attach` INTEGER NOT NULL, `update` INTEGER NOT NULL, `delete` INTEGER NOT NULL, `reply` INTEGER NOT NULL, FOREIGN KEY(`discussionTopicHeaderId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attach", + "columnName": "attach", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delete", + "columnName": "delete", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reply", + "columnName": "reply", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicHeaderId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicRemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionId`, `remoteFileId`), FOREIGN KEY(`discussionId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionId", + "columnName": "discussionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicSectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionTopicId` INTEGER NOT NULL, `sectionId` INTEGER NOT NULL, PRIMARY KEY(`discussionTopicId`, `sectionId`), FOREIGN KEY(`discussionTopicId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionTopicId", + "columnName": "discussionTopicId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionTopicId", + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "EnrollmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `role` TEXT NOT NULL, `type` TEXT NOT NULL, `courseId` INTEGER, `courseSectionId` INTEGER, `enrollmentState` TEXT, `userId` INTEGER NOT NULL, `computedCurrentScore` REAL, `computedFinalScore` REAL, `computedCurrentGrade` TEXT, `computedFinalGrade` TEXT, `multipleGradingPeriodsEnabled` INTEGER NOT NULL, `totalsForAllGradingPeriodsOption` INTEGER NOT NULL, `currentPeriodComputedCurrentScore` REAL, `currentPeriodComputedFinalScore` REAL, `currentPeriodComputedCurrentGrade` TEXT, `currentPeriodComputedFinalGrade` TEXT, `currentGradingPeriodId` INTEGER NOT NULL, `currentGradingPeriodTitle` TEXT, `associatedUserId` INTEGER NOT NULL, `lastActivityAt` INTEGER, `limitPrivilegesToCourseSection` INTEGER NOT NULL, `observedUserId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`observedUserId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseSectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "computedCurrentScore", + "columnName": "computedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedFinalScore", + "columnName": "computedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedCurrentGrade", + "columnName": "computedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "computedFinalGrade", + "columnName": "computedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "multipleGradingPeriodsEnabled", + "columnName": "multipleGradingPeriodsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalsForAllGradingPeriodsOption", + "columnName": "totalsForAllGradingPeriodsOption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPeriodComputedCurrentScore", + "columnName": "currentPeriodComputedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalScore", + "columnName": "currentPeriodComputedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedCurrentGrade", + "columnName": "currentPeriodComputedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalGrade", + "columnName": "currentPeriodComputedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentGradingPeriodId", + "columnName": "currentGradingPeriodId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentGradingPeriodTitle", + "columnName": "currentGradingPeriodTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "associatedUserId", + "columnName": "associatedUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivityAt", + "columnName": "lastActivityAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "limitPrivilegesToCourseSection", + "columnName": "limitPrivilegesToCourseSection", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "observedUserId", + "columnName": "observedUserId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "observedUserId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "courseSectionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileFolderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `createdDate` INTEGER, `updatedDate` INTEGER, `unlockDate` INTEGER, `lockDate` INTEGER, `isLocked` INTEGER NOT NULL, `isHidden` INTEGER NOT NULL, `isLockedForUser` INTEGER NOT NULL, `isHiddenForUser` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `size` INTEGER NOT NULL, `contentType` TEXT, `url` TEXT, `displayName` TEXT, `thumbnailUrl` TEXT, `parentFolderId` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `filesCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `foldersCount` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `foldersUrl` TEXT, `filesUrl` TEXT, `fullName` TEXT, `forSubmissions` INTEGER NOT NULL, `canUpload` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedDate", + "columnName": "updatedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unlockDate", + "columnName": "unlockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockDate", + "columnName": "lockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLockedForUser", + "columnName": "isLockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHiddenForUser", + "columnName": "isHiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentFolderId", + "columnName": "parentFolderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filesCount", + "columnName": "filesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "foldersCount", + "columnName": "foldersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "foldersUrl", + "columnName": "foldersUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesUrl", + "columnName": "filesUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "forSubmissions", + "columnName": "forSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canUpload", + "columnName": "canUpload", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "EditDashboardItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `name` TEXT NOT NULL, `isFavorite` INTEGER NOT NULL, `enrollmentState` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ExternalToolAttributesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `url` TEXT, `newTab` INTEGER NOT NULL, `resourceLinkid` TEXT, `contentId` INTEGER, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "newTab", + "columnName": "newTab", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceLinkid", + "columnName": "resourceLinkid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`enrollmentId` INTEGER NOT NULL, `htmlUrl` TEXT NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, PRIMARY KEY(`enrollmentId`), FOREIGN KEY(`enrollmentId`) REFERENCES `EnrollmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "enrollmentId", + "columnName": "enrollmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "enrollmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "EnrollmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "enrollmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `startDate` TEXT, `endDate` TEXT, `weight` REAL NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endDate", + "columnName": "endDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `avatarUrl` TEXT, `isPublic` INTEGER NOT NULL, `membersCount` INTEGER NOT NULL, `joinLevel` TEXT, `courseId` INTEGER NOT NULL, `accountId` INTEGER NOT NULL, `role` TEXT, `groupCategoryId` INTEGER NOT NULL, `storageQuotaMb` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `concluded` INTEGER NOT NULL, `canAccess` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "membersCount", + "columnName": "membersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinLevel", + "columnName": "joinLevel", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "storageQuotaMb", + "columnName": "storageQuotaMb", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concluded", + "columnName": "concluded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canAccess", + "columnName": "canAccess", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupUserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LocalFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `createdDate` INTEGER NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MasteryPathAssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `overrideId` INTEGER NOT NULL, `assignmentSetId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentSetId`) REFERENCES `AssignmentSetEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "overrideId", + "columnName": "overrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentSetId", + "columnName": "assignmentSetId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentSetEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentSetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MasteryPathEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `selectedSetId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectedSetId", + "columnName": "selectedSetId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleContentDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `pointsPossible` TEXT, `dueAt` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `moduleId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `title` TEXT, `indent` INTEGER NOT NULL, `type` TEXT, `htmlUrl` TEXT, `url` TEXT, `published` INTEGER, `contentId` INTEGER NOT NULL, `externalUrl` TEXT, `pageUrl` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`moduleId`) REFERENCES `ModuleObjectEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "indent", + "columnName": "indent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageUrl", + "columnName": "pageUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleObjectEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleObjectEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `name` TEXT, `unlockAt` TEXT, `sequentialProgress` INTEGER NOT NULL, `prerequisiteIds` TEXT, `state` TEXT, `completedAt` TEXT, `published` INTEGER, `itemCount` INTEGER NOT NULL, `itemsUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sequentialProgress", + "columnName": "sequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prerequisiteIds", + "columnName": "prerequisiteIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "completedAt", + "columnName": "completedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemCount", + "columnName": "itemCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemsUrl", + "columnName": "itemsUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "NeedsGradingCountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sectionId` INTEGER NOT NULL, `needsGradingCount` INTEGER NOT NULL, PRIMARY KEY(`sectionId`), FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `createdAt` INTEGER, `updatedAt` INTEGER, `hideFromStudents` INTEGER NOT NULL, `status` TEXT, `body` TEXT, `frontPage` INTEGER NOT NULL, `published` INTEGER NOT NULL, `editingRoles` TEXT, `htmlUrl` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hideFromStudents", + "columnName": "hideFromStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "frontPage", + "columnName": "frontPage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editingRoles", + "columnName": "editingRoles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PlannerItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER, `groupId` INTEGER, `userId` INTEGER, `contextType` TEXT, `contextName` TEXT, `plannableType` TEXT NOT NULL, `plannableId` INTEGER NOT NULL, `plannableTitle` TEXT, `plannableDetails` TEXT, `plannableTodoDate` TEXT, `plannableEndAt` INTEGER, `plannableAllDay` INTEGER, `plannableCourseId` INTEGER, `plannableGroupId` INTEGER, `plannableUserId` INTEGER, `plannableDate` INTEGER NOT NULL, `htmlUrl` TEXT, `submissionStateSubmitted` INTEGER, `submissionStateExcused` INTEGER, `submissionStateGraded` INTEGER, `newActivity` INTEGER, `plannerOverrideId` INTEGER, `plannerOverrideMarkedComplete` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextName", + "columnName": "contextName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableType", + "columnName": "plannableType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "plannableId", + "columnName": "plannableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannableTitle", + "columnName": "plannableTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableDetails", + "columnName": "plannableDetails", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableTodoDate", + "columnName": "plannableTodoDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableEndAt", + "columnName": "plannableEndAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableAllDay", + "columnName": "plannableAllDay", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableCourseId", + "columnName": "plannableCourseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableGroupId", + "columnName": "plannableGroupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableUserId", + "columnName": "plannableUserId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableDate", + "columnName": "plannableDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionStateSubmitted", + "columnName": "submissionStateSubmitted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionStateExcused", + "columnName": "submissionStateExcused", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionStateGraded", + "columnName": "submissionStateGraded", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "newActivity", + "columnName": "newActivity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannerOverrideId", + "columnName": "plannerOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannerOverrideMarkedComplete", + "columnName": "plannerOverrideMarkedComplete", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PlannerOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `plannableType` TEXT NOT NULL, `plannableId` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, `markedComplete` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannableType", + "columnName": "plannableType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "plannableId", + "columnName": "plannableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "markedComplete", + "columnName": "markedComplete", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `displayName` TEXT, `fileName` TEXT, `contentType` TEXT, `url` TEXT, `size` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `unlockAt` TEXT, `locked` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `lockAt` TEXT, `hiddenForUser` INTEGER NOT NULL, `thumbnailUrl` TEXT, `modifiedAt` TEXT, `lockedForUser` INTEGER NOT NULL, `previewUrl` TEXT, `lockExplanation` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hiddenForUser", + "columnName": "hiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "modifiedAt", + "columnName": "modifiedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RubricCriterionAssessmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `assignmentId` INTEGER NOT NULL, `ratingId` TEXT, `points` REAL, `comments` TEXT, PRIMARY KEY(`id`, `assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingId", + "columnName": "ratingId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "comments", + "columnName": "comments", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `criterionUseRange` INTEGER NOT NULL, `ignoreForScoring` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "criterionUseRange", + "columnName": "criterionUseRange", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ignoreForScoring", + "columnName": "ignoreForScoring", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionRatingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `rubricCriterionId` TEXT NOT NULL, PRIMARY KEY(`id`, `rubricCriterionId`), FOREIGN KEY(`rubricCriterionId`) REFERENCES `RubricCriterionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rubricCriterionId", + "columnName": "rubricCriterionId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "rubricCriterionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "RubricCriterionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "rubricCriterionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `pointsPossible` REAL NOT NULL, `title` TEXT NOT NULL, `isReusable` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `isReadOnly` INTEGER NOT NULL, `freeFormCriterionComments` INTEGER NOT NULL, `hideScoreTotal` INTEGER NOT NULL, `hidePoints` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isReusable", + "columnName": "isReusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isReadOnly", + "columnName": "isReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hideScoreTotal", + "columnName": "hideScoreTotal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidePoints", + "columnName": "hidePoints", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemAssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentOverrideId` INTEGER NOT NULL, `scheduleItemId` TEXT NOT NULL, PRIMARY KEY(`assignmentOverrideId`, `scheduleItemId`), FOREIGN KEY(`assignmentOverrideId`) REFERENCES `AssignmentOverrideEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`scheduleItemId`) REFERENCES `ScheduleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduleItemId", + "columnName": "scheduleItemId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentOverrideId", + "scheduleItemId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentOverrideEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentOverrideId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "ScheduleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "scheduleItemId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `description` TEXT, `startAt` TEXT, `endAt` TEXT, `isAllDay` INTEGER NOT NULL, `allDayAt` TEXT, `locationAddress` TEXT, `locationName` TEXT, `htmlUrl` TEXT, `contextCode` TEXT, `effectiveContextCode` TEXT, `isHidden` INTEGER NOT NULL, `importantDates` INTEGER NOT NULL, `assignmentId` INTEGER, `type` TEXT NOT NULL, `itemType` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayAt", + "columnName": "allDayAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationAddress", + "columnName": "locationAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationName", + "columnName": "locationName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextCode", + "columnName": "contextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effectiveContextCode", + "columnName": "effectiveContextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "importantDates", + "columnName": "importantDates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemType", + "columnName": "itemType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER, `startAt` TEXT, `endAt` TEXT, `totalStudents` INTEGER NOT NULL, `restrictEnrollmentsToSectionDates` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "totalStudents", + "columnName": "totalStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "restrictEnrollmentsToSectionDates", + "columnName": "restrictEnrollmentsToSectionDates", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionDiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`submissionId` INTEGER NOT NULL, `discussionEntryId` INTEGER NOT NULL, PRIMARY KEY(`submissionId`, `discussionEntryId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "submissionId", + "discussionEntryId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `attempt` INTEGER NOT NULL, `submittedAt` INTEGER, `commentCreated` INTEGER, `mediaContentType` TEXT, `mediaCommentUrl` TEXT, `mediaCommentDisplay` TEXT, `body` TEXT, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, `workflowState` TEXT, `submissionType` TEXT, `previewUrl` TEXT, `url` TEXT, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `mediaCommentId` TEXT, `assignmentId` INTEGER NOT NULL, `userId` INTEGER, `graderId` INTEGER, `groupId` INTEGER, `pointsDeducted` REAL, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `postedAt` INTEGER, `gradingPeriodId` INTEGER, `customGradeStatusId` INTEGER, `hasSubAssignmentSubmissions` INTEGER NOT NULL, PRIMARY KEY(`id`, `attempt`), FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submittedAt", + "columnName": "submittedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "commentCreated", + "columnName": "commentCreated", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaContentType", + "columnName": "mediaContentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentUrl", + "columnName": "mediaCommentUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentDisplay", + "columnName": "mediaCommentDisplay", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionType", + "columnName": "submissionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "graderId", + "columnName": "graderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pointsDeducted", + "columnName": "pointsDeducted", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedAt", + "columnName": "postedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasSubAssignmentSubmissions", + "columnName": "hasSubAssignmentSubmissions", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "attempt" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `autoSyncEnabled` INTEGER NOT NULL, `syncFrequency` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoSyncEnabled", + "columnName": "autoSyncEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncFrequency", + "columnName": "syncFrequency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifiOnly", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TabEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `label` TEXT, `type` TEXT NOT NULL, `htmlUrl` TEXT, `externalUrl` TEXT, `visibility` TEXT NOT NULL, `isHidden` INTEGER NOT NULL, `position` INTEGER NOT NULL, `ltiUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ltiUrl", + "columnName": "ltiUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "TermEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `startAt` TEXT, `endAt` TEXT, `isGroupTerm` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGroupTerm", + "columnName": "isGroupTerm", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserCalendarEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ics` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ics", + "columnName": "ics", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `shortName` TEXT, `loginId` TEXT, `avatarUrl` TEXT, `primaryEmail` TEXT, `email` TEXT, `sortableName` TEXT, `bio` TEXT, `enrollmentIndex` INTEGER NOT NULL, `lastLogin` TEXT, `locale` TEXT, `effective_locale` TEXT, `pronouns` TEXT, `k5User` INTEGER NOT NULL, `rootAccount` TEXT, `isFakeStudent` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginId", + "columnName": "loginId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "primaryEmail", + "columnName": "primaryEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortableName", + "columnName": "sortableName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bio", + "columnName": "bio", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enrollmentIndex", + "columnName": "enrollmentIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastLogin", + "columnName": "lastLogin", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effective_locale", + "columnName": "effective_locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "k5User", + "columnName": "k5User", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rootAccount", + "columnName": "rootAccount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFakeStudent", + "columnName": "isFakeStudent", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "QuizEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `mobileUrl` TEXT, `htmlUrl` TEXT, `description` TEXT, `quizType` TEXT, `assignmentGroupId` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `questionCount` INTEGER NOT NULL, `pointsPossible` TEXT, `isLockQuestionsAfterAnswering` INTEGER NOT NULL, `dueAt` TEXT, `timeLimit` INTEGER NOT NULL, `shuffleAnswers` INTEGER NOT NULL, `showCorrectAnswers` INTEGER NOT NULL, `scoringPolicy` TEXT, `accessCode` TEXT, `ipFilter` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `hideResults` TEXT, `showCorrectAnswersAt` TEXT, `hideCorrectAnswersAt` TEXT, `unlockAt` TEXT, `oneTimeResults` INTEGER NOT NULL, `lockAt` TEXT, `questionTypes` TEXT NOT NULL, `hasAccessCode` INTEGER NOT NULL, `oneQuestionAtATime` INTEGER NOT NULL, `requireLockdownBrowser` INTEGER NOT NULL, `requireLockdownBrowserForResults` INTEGER NOT NULL, `allowAnonymousSubmissions` INTEGER NOT NULL, `published` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `isOnlyVisibleToOverrides` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mobileUrl", + "columnName": "mobileUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizType", + "columnName": "quizType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionCount", + "columnName": "questionCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isLockQuestionsAfterAnswering", + "columnName": "isLockQuestionsAfterAnswering", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeLimit", + "columnName": "timeLimit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shuffleAnswers", + "columnName": "shuffleAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showCorrectAnswers", + "columnName": "showCorrectAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringPolicy", + "columnName": "scoringPolicy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accessCode", + "columnName": "accessCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ipFilter", + "columnName": "ipFilter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideResults", + "columnName": "hideResults", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "showCorrectAnswersAt", + "columnName": "showCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideCorrectAnswersAt", + "columnName": "hideCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oneTimeResults", + "columnName": "oneTimeResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "questionTypes", + "columnName": "questionTypes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasAccessCode", + "columnName": "hasAccessCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneQuestionAtATime", + "columnName": "oneQuestionAtATime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowser", + "columnName": "requireLockdownBrowser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowserForResults", + "columnName": "requireLockdownBrowserForResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowAnonymousSubmissions", + "columnName": "allowAnonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isOnlyVisibleToOverrides", + "columnName": "isOnlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockInfoEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `modulePrerequisiteNames` TEXT, `unlockAt` TEXT, `lockedModuleId` INTEGER, `assignmentId` INTEGER, `moduleId` INTEGER, `pageId` INTEGER, FOREIGN KEY(`moduleId`) REFERENCES `ModuleContentDetailsEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`pageId`) REFERENCES `PageEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "modulePrerequisiteNames", + "columnName": "modulePrerequisiteNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleContentDetailsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "PageEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pageId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockedModuleEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `unlockAt` TEXT, `isRequireSequentialProgress` INTEGER NOT NULL, `lockInfoId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`lockInfoId`) REFERENCES `LockInfoEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isRequireSequentialProgress", + "columnName": "isRequireSequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockInfoId", + "columnName": "lockInfoId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockInfoEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockInfoId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleNameEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `lockedModuleId` INTEGER NOT NULL, FOREIGN KEY(`lockedModuleId`) REFERENCES `LockedModuleEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockedModuleEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockedModuleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleCompletionRequirementEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT, `minScore` REAL NOT NULL, `maxScore` REAL NOT NULL, `completed` INTEGER, `moduleId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "minScore", + "columnName": "minScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxScore", + "columnName": "maxScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `fileName` TEXT, `courseId` INTEGER NOT NULL, `url` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "ConferenceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `conferenceKey` TEXT, `conferenceType` TEXT, `description` TEXT, `duration` INTEGER NOT NULL, `endedAt` INTEGER, `hasAdvancedSettings` INTEGER NOT NULL, `joinUrl` TEXT, `longRunning` INTEGER NOT NULL, `startedAt` INTEGER, `title` TEXT, `url` TEXT, `contextType` TEXT NOT NULL, `contextId` INTEGER NOT NULL, `record` INTEGER, `users` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conferenceKey", + "columnName": "conferenceKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "conferenceType", + "columnName": "conferenceType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAdvancedSettings", + "columnName": "hasAdvancedSettings", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinUrl", + "columnName": "joinUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longRunning", + "columnName": "longRunning", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startedAt", + "columnName": "startedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "record", + "columnName": "record", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "users", + "columnName": "users", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ConferenceRecordingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`recordingId` TEXT NOT NULL, `conferenceId` INTEGER NOT NULL, `createdAtMillis` INTEGER NOT NULL, `durationMinutes` INTEGER NOT NULL, `playbackUrl` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`recordingId`), FOREIGN KEY(`conferenceId`) REFERENCES `ConferenceEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "recordingId", + "columnName": "recordingId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conferenceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAtMillis", + "columnName": "createdAtMillis", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durationMinutes", + "columnName": "durationMinutes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playbackUrl", + "columnName": "playbackUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "recordingId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ConferenceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "conferenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFeaturesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `features` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contentType` TEXT, `filename` TEXT, `displayName` TEXT, `url` TEXT, `thumbnailUrl` TEXT, `previewUrl` TEXT, `createdAt` INTEGER, `size` INTEGER NOT NULL, `workerId` TEXT, `submissionCommentId` INTEGER, `submissionId` INTEGER, `attempt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionCommentId`) REFERENCES `SubmissionCommentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionCommentId", + "columnName": "submissionCommentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionCommentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionCommentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MediaCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mediaId` TEXT NOT NULL, `submissionId` INTEGER NOT NULL, `attemptId` INTEGER NOT NULL, `displayName` TEXT, `url` TEXT, `mediaType` TEXT, `contentType` TEXT, PRIMARY KEY(`mediaId`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "mediaId", + "columnName": "mediaId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaType", + "columnName": "mediaType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "mediaId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "AuthorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, `pronouns` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `authorId` INTEGER NOT NULL, `authorName` TEXT, `authorPronouns` TEXT, `comment` TEXT, `createdAt` INTEGER, `mediaCommentId` TEXT, `attemptId` INTEGER, `submissionId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorName", + "columnName": "authorName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorPronouns", + "columnName": "authorPronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "DiscussionTopicEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unreadEntries` TEXT NOT NULL, `participantIds` TEXT NOT NULL, `viewIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadEntries", + "columnName": "unreadEntries", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantIds", + "columnName": "participantIds", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viewIds", + "columnName": "viewIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CourseSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `tabs` TEXT NOT NULL, `additionalFilesStarted` INTEGER NOT NULL, `progressState` TEXT NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "additionalFilesStarted", + "columnName": "additionalFilesStarted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FileSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `additionalFile` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseSyncProgressEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "fileId", + "columnName": "fileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "additionalFile", + "columnName": "additionalFile", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncProgressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "StudioMediaProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ltiLaunchId` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "ltiLaunchId", + "columnName": "ltiLaunchId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CustomGradeStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CheckpointEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `assignmentId` INTEGER NOT NULL, `name` TEXT, `tag` TEXT, `pointsPossible` REAL, `dueAt` TEXT, `onlyVisibleToOverrides` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubAssignmentSubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `submissionId` INTEGER NOT NULL, `submissionAttempt` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `latePolicyStatus` TEXT, `customGradeStatusId` INTEGER, `subAssignmentTag` TEXT, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `userId` INTEGER NOT NULL, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, FOREIGN KEY(`submissionId`, `submissionAttempt`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionAttempt", + "columnName": "submissionAttempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latePolicyStatus", + "columnName": "latePolicyStatus", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subAssignmentTag", + "columnName": "subAssignmentTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "submissionAttempt" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '452f8a3a37230a66a3dafed1956528e6')" + ] + } +} \ No newline at end of file diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/8.json b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/8.json new file mode 100644 index 0000000000..e3de8642eb --- /dev/null +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/8.json @@ -0,0 +1,6025 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "371b12b510101fec6ff4b7155ac8b092", + "entities": [ + { + "tableName": "AssignmentDueDateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `assignmentOverrideId` INTEGER, `dueAt` TEXT, `title` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `isBase` INTEGER NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isBase", + "columnName": "isBase", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `submissionTypesRaw` TEXT NOT NULL, `dueAt` TEXT, `pointsPossible` REAL NOT NULL, `courseId` INTEGER NOT NULL, `isGradeGroupsIndividually` INTEGER NOT NULL, `gradingType` TEXT, `needsGradingCount` INTEGER NOT NULL, `htmlUrl` TEXT, `url` TEXT, `quizId` INTEGER NOT NULL, `isUseRubricForGrading` INTEGER NOT NULL, `rubricSettingsId` INTEGER, `allowedExtensions` TEXT NOT NULL, `submissionId` INTEGER, `assignmentGroupId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `isPeerReviews` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, `lockExplanation` TEXT, `discussionTopicHeaderId` INTEGER, `freeFormCriterionComments` INTEGER NOT NULL, `published` INTEGER NOT NULL, `groupCategoryId` INTEGER NOT NULL, `userSubmitted` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `onlyVisibleToOverrides` INTEGER NOT NULL, `anonymousPeerReviews` INTEGER NOT NULL, `moderatedGrading` INTEGER NOT NULL, `anonymousGrading` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `plannerOverrideId` INTEGER, `isStudioEnabled` INTEGER NOT NULL, `inClosedGradingPeriod` INTEGER NOT NULL, `annotatableAttachmentId` INTEGER NOT NULL, `anonymousSubmissions` INTEGER NOT NULL, `omitFromFinalGrade` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentGroupId`) REFERENCES `AssignmentGroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionTypesRaw", + "columnName": "submissionTypesRaw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeGroupsIndividually", + "columnName": "isGradeGroupsIndividually", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingType", + "columnName": "gradingType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizId", + "columnName": "quizId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUseRubricForGrading", + "columnName": "isUseRubricForGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricSettingsId", + "columnName": "rubricSettingsId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "allowedExtensions", + "columnName": "allowedExtensions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPeerReviews", + "columnName": "isPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userSubmitted", + "columnName": "userSubmitted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousPeerReviews", + "columnName": "anonymousPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moderatedGrading", + "columnName": "moderatedGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousGrading", + "columnName": "anonymousGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannerOverrideId", + "columnName": "plannerOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isStudioEnabled", + "columnName": "isStudioEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inClosedGradingPeriod", + "columnName": "inClosedGradingPeriod", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "annotatableAttachmentId", + "columnName": "annotatableAttachmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousSubmissions", + "columnName": "anonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "omitFromFinalGrade", + "columnName": "omitFromFinalGrade", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentGroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentGroupId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentGroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `position` INTEGER NOT NULL, `groupWeight` REAL NOT NULL, `rules` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupWeight", + "columnName": "groupWeight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rules", + "columnName": "rules", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `title` TEXT, `dueAt` INTEGER, `isAllDay` INTEGER NOT NULL, `allDayDate` TEXT, `unlockAt` INTEGER, `lockAt` INTEGER, `courseSectionId` INTEGER NOT NULL, `groupId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayDate", + "columnName": "allDayDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentRubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `rubricId` TEXT NOT NULL, PRIMARY KEY(`assignmentId`, `rubricId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricId", + "columnName": "rubricId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId", + "rubricId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentScoreStatisticsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `mean` REAL NOT NULL, `min` REAL NOT NULL, `max` REAL NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mean", + "columnName": "mean", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "min", + "columnName": "min", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "max", + "columnName": "max", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentSetEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `scoringRangeId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `position` INTEGER NOT NULL, `masteryPathId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`masteryPathId`) REFERENCES `MasteryPathEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringRangeId", + "columnName": "scoringRangeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "masteryPathId", + "columnName": "masteryPathId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "MasteryPathEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "masteryPathId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `originalName` TEXT, `courseCode` TEXT, `startAt` TEXT, `endAt` TEXT, `syllabusBody` TEXT, `hideFinalGrades` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `license` TEXT NOT NULL, `termId` INTEGER, `needsGradingCount` INTEGER NOT NULL, `isApplyAssignmentGroupWeights` INTEGER NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, `isFavorite` INTEGER NOT NULL, `accessRestrictedByDate` INTEGER NOT NULL, `imageUrl` TEXT, `bannerImageUrl` TEXT, `isWeightedGradingPeriods` INTEGER NOT NULL, `hasGradingPeriods` INTEGER NOT NULL, `homePage` TEXT, `restrictEnrollmentsToCourseDate` INTEGER NOT NULL, `workflowState` TEXT, `homeroomCourse` INTEGER NOT NULL, `courseColor` TEXT, `gradingScheme` TEXT, `pointsBasedGradingScheme` INTEGER NOT NULL, `scalingFactor` REAL NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`termId`) REFERENCES `TermEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syllabusBody", + "columnName": "syllabusBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideFinalGrades", + "columnName": "hideFinalGrades", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "license", + "columnName": "license", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "termId", + "columnName": "termId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isApplyAssignmentGroupWeights", + "columnName": "isApplyAssignmentGroupWeights", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessRestrictedByDate", + "columnName": "accessRestrictedByDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imageUrl", + "columnName": "imageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bannerImageUrl", + "columnName": "bannerImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isWeightedGradingPeriods", + "columnName": "isWeightedGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasGradingPeriods", + "columnName": "hasGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homePage", + "columnName": "homePage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "restrictEnrollmentsToCourseDate", + "columnName": "restrictEnrollmentsToCourseDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homeroomCourse", + "columnName": "homeroomCourse", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseColor", + "columnName": "courseColor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "gradingScheme", + "columnName": "gradingScheme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsBasedGradingScheme", + "columnName": "pointsBasedGradingScheme", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scalingFactor", + "columnName": "scalingFactor", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "TermEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "termId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFilesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`courseId`, `url`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "url" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "CourseGradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `gradingPeriodId` INTEGER NOT NULL, PRIMARY KEY(`courseId`, `gradingPeriodId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`gradingPeriodId`) REFERENCES `GradingPeriodEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "gradingPeriodId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "GradingPeriodEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "gradingPeriodId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseSummary` INTEGER, `restrictQuantitativeData` INTEGER NOT NULL, PRIMARY KEY(`courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseSummary", + "columnName": "courseSummary", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "restrictQuantitativeData", + "columnName": "restrictQuantitativeData", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `fullContentSync` INTEGER NOT NULL, `tabs` TEXT NOT NULL, `fullFileSync` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullContentSync", + "columnName": "fullContentSync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullFileSync", + "columnName": "fullFileSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DashboardCardEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isK5Subject` INTEGER NOT NULL, `shortName` TEXT, `originalName` TEXT, `courseCode` TEXT, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isK5Subject", + "columnName": "isK5Subject", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionEntryAttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionEntryId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionEntryId`, `remoteFileId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionEntryId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `updatedAt` TEXT, `createdAt` TEXT, `authorId` INTEGER, `description` TEXT, `userId` INTEGER NOT NULL, `parentId` INTEGER NOT NULL, `message` TEXT, `deleted` INTEGER NOT NULL, `totalChildren` INTEGER NOT NULL, `unreadChildren` INTEGER NOT NULL, `ratingCount` INTEGER NOT NULL, `ratingSum` INTEGER NOT NULL, `editorId` INTEGER NOT NULL, `_hasRated` INTEGER NOT NULL, `replyIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChildren", + "columnName": "totalChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadChildren", + "columnName": "unreadChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingCount", + "columnName": "ratingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingSum", + "columnName": "ratingSum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editorId", + "columnName": "editorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "_hasRated", + "columnName": "_hasRated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyIds", + "columnName": "replyIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionParticipantEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `pronouns` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionTopicHeaderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `discussionType` TEXT, `title` TEXT, `message` TEXT, `htmlUrl` TEXT, `postedDate` INTEGER, `delayedPostDate` INTEGER, `lastReplyDate` INTEGER, `requireInitialPost` INTEGER NOT NULL, `discussionSubentryCount` INTEGER NOT NULL, `readState` TEXT, `unreadCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `assignmentId` INTEGER, `locked` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `pinned` INTEGER NOT NULL, `authorId` INTEGER, `podcastUrl` TEXT, `groupCategoryId` TEXT, `announcement` INTEGER NOT NULL, `permissionId` INTEGER, `published` INTEGER NOT NULL, `allowRating` INTEGER NOT NULL, `onlyGradersCanRate` INTEGER NOT NULL, `sortByRating` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `lockAt` INTEGER, `userCanSeePosts` INTEGER NOT NULL, `specificSections` TEXT, `anonymousState` TEXT, `replyRequiredCount` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`authorId`) REFERENCES `DiscussionParticipantEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`permissionId`) REFERENCES `DiscussionTopicPermissionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionType", + "columnName": "discussionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedDate", + "columnName": "postedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "delayedPostDate", + "columnName": "delayedPostDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastReplyDate", + "columnName": "lastReplyDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "requireInitialPost", + "columnName": "requireInitialPost", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionSubentryCount", + "columnName": "discussionSubentryCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readState", + "columnName": "readState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unreadCount", + "columnName": "unreadCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "podcastUrl", + "columnName": "podcastUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "announcement", + "columnName": "announcement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "permissionId", + "columnName": "permissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowRating", + "columnName": "allowRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyGradersCanRate", + "columnName": "onlyGradersCanRate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortByRating", + "columnName": "sortByRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userCanSeePosts", + "columnName": "userCanSeePosts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "specificSections", + "columnName": "specificSections", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "anonymousState", + "columnName": "anonymousState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replyRequiredCount", + "columnName": "replyRequiredCount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionParticipantEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "authorId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "DiscussionTopicPermissionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "permissionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicPermissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `discussionTopicHeaderId` INTEGER NOT NULL, `attach` INTEGER NOT NULL, `update` INTEGER NOT NULL, `delete` INTEGER NOT NULL, `reply` INTEGER NOT NULL, FOREIGN KEY(`discussionTopicHeaderId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attach", + "columnName": "attach", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delete", + "columnName": "delete", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reply", + "columnName": "reply", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicHeaderId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicRemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionId`, `remoteFileId`), FOREIGN KEY(`discussionId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionId", + "columnName": "discussionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicSectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionTopicId` INTEGER NOT NULL, `sectionId` INTEGER NOT NULL, PRIMARY KEY(`discussionTopicId`, `sectionId`), FOREIGN KEY(`discussionTopicId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionTopicId", + "columnName": "discussionTopicId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionTopicId", + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "EnrollmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `role` TEXT NOT NULL, `type` TEXT NOT NULL, `courseId` INTEGER, `courseSectionId` INTEGER, `enrollmentState` TEXT, `userId` INTEGER NOT NULL, `computedCurrentScore` REAL, `computedFinalScore` REAL, `computedCurrentGrade` TEXT, `computedFinalGrade` TEXT, `multipleGradingPeriodsEnabled` INTEGER NOT NULL, `totalsForAllGradingPeriodsOption` INTEGER NOT NULL, `currentPeriodComputedCurrentScore` REAL, `currentPeriodComputedFinalScore` REAL, `currentPeriodComputedCurrentGrade` TEXT, `currentPeriodComputedFinalGrade` TEXT, `currentGradingPeriodId` INTEGER NOT NULL, `currentGradingPeriodTitle` TEXT, `associatedUserId` INTEGER NOT NULL, `lastActivityAt` INTEGER, `limitPrivilegesToCourseSection` INTEGER NOT NULL, `observedUserId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`observedUserId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseSectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "computedCurrentScore", + "columnName": "computedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedFinalScore", + "columnName": "computedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedCurrentGrade", + "columnName": "computedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "computedFinalGrade", + "columnName": "computedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "multipleGradingPeriodsEnabled", + "columnName": "multipleGradingPeriodsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalsForAllGradingPeriodsOption", + "columnName": "totalsForAllGradingPeriodsOption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPeriodComputedCurrentScore", + "columnName": "currentPeriodComputedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalScore", + "columnName": "currentPeriodComputedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedCurrentGrade", + "columnName": "currentPeriodComputedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalGrade", + "columnName": "currentPeriodComputedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentGradingPeriodId", + "columnName": "currentGradingPeriodId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentGradingPeriodTitle", + "columnName": "currentGradingPeriodTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "associatedUserId", + "columnName": "associatedUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivityAt", + "columnName": "lastActivityAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "limitPrivilegesToCourseSection", + "columnName": "limitPrivilegesToCourseSection", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "observedUserId", + "columnName": "observedUserId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "observedUserId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "courseSectionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileFolderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `createdDate` INTEGER, `updatedDate` INTEGER, `unlockDate` INTEGER, `lockDate` INTEGER, `isLocked` INTEGER NOT NULL, `isHidden` INTEGER NOT NULL, `isLockedForUser` INTEGER NOT NULL, `isHiddenForUser` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `size` INTEGER NOT NULL, `contentType` TEXT, `url` TEXT, `displayName` TEXT, `thumbnailUrl` TEXT, `parentFolderId` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `filesCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `foldersCount` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `foldersUrl` TEXT, `filesUrl` TEXT, `fullName` TEXT, `forSubmissions` INTEGER NOT NULL, `canUpload` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedDate", + "columnName": "updatedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unlockDate", + "columnName": "unlockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockDate", + "columnName": "lockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLockedForUser", + "columnName": "isLockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHiddenForUser", + "columnName": "isHiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentFolderId", + "columnName": "parentFolderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filesCount", + "columnName": "filesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "foldersCount", + "columnName": "foldersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "foldersUrl", + "columnName": "foldersUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesUrl", + "columnName": "filesUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "forSubmissions", + "columnName": "forSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canUpload", + "columnName": "canUpload", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "EditDashboardItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `name` TEXT NOT NULL, `isFavorite` INTEGER NOT NULL, `enrollmentState` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ExternalToolAttributesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `url` TEXT, `newTab` INTEGER NOT NULL, `resourceLinkid` TEXT, `contentId` INTEGER, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "newTab", + "columnName": "newTab", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceLinkid", + "columnName": "resourceLinkid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`enrollmentId` INTEGER NOT NULL, `htmlUrl` TEXT NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, PRIMARY KEY(`enrollmentId`), FOREIGN KEY(`enrollmentId`) REFERENCES `EnrollmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "enrollmentId", + "columnName": "enrollmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "enrollmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "EnrollmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "enrollmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `startDate` TEXT, `endDate` TEXT, `weight` REAL NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endDate", + "columnName": "endDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `avatarUrl` TEXT, `isPublic` INTEGER NOT NULL, `membersCount` INTEGER NOT NULL, `joinLevel` TEXT, `courseId` INTEGER NOT NULL, `accountId` INTEGER NOT NULL, `role` TEXT, `groupCategoryId` INTEGER NOT NULL, `storageQuotaMb` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `concluded` INTEGER NOT NULL, `canAccess` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "membersCount", + "columnName": "membersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinLevel", + "columnName": "joinLevel", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "storageQuotaMb", + "columnName": "storageQuotaMb", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concluded", + "columnName": "concluded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canAccess", + "columnName": "canAccess", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupUserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LocalFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `createdDate` INTEGER NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MasteryPathAssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `overrideId` INTEGER NOT NULL, `assignmentSetId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentSetId`) REFERENCES `AssignmentSetEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "overrideId", + "columnName": "overrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentSetId", + "columnName": "assignmentSetId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentSetEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentSetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MasteryPathEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `selectedSetId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectedSetId", + "columnName": "selectedSetId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleContentDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `pointsPossible` TEXT, `dueAt` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `moduleId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `title` TEXT, `indent` INTEGER NOT NULL, `type` TEXT, `htmlUrl` TEXT, `url` TEXT, `published` INTEGER, `contentId` INTEGER NOT NULL, `externalUrl` TEXT, `pageUrl` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`moduleId`) REFERENCES `ModuleObjectEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "indent", + "columnName": "indent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageUrl", + "columnName": "pageUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleObjectEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleObjectEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `name` TEXT, `unlockAt` TEXT, `sequentialProgress` INTEGER NOT NULL, `prerequisiteIds` TEXT, `state` TEXT, `completedAt` TEXT, `published` INTEGER, `itemCount` INTEGER NOT NULL, `itemsUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sequentialProgress", + "columnName": "sequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prerequisiteIds", + "columnName": "prerequisiteIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "completedAt", + "columnName": "completedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemCount", + "columnName": "itemCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemsUrl", + "columnName": "itemsUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "NeedsGradingCountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sectionId` INTEGER NOT NULL, `needsGradingCount` INTEGER NOT NULL, PRIMARY KEY(`sectionId`), FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `createdAt` INTEGER, `updatedAt` INTEGER, `hideFromStudents` INTEGER NOT NULL, `status` TEXT, `body` TEXT, `frontPage` INTEGER NOT NULL, `published` INTEGER NOT NULL, `editingRoles` TEXT, `htmlUrl` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hideFromStudents", + "columnName": "hideFromStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "frontPage", + "columnName": "frontPage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editingRoles", + "columnName": "editingRoles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PlannerItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER, `groupId` INTEGER, `userId` INTEGER, `contextType` TEXT, `contextName` TEXT, `plannableType` TEXT NOT NULL, `plannableId` INTEGER NOT NULL, `plannableTitle` TEXT, `plannableDetails` TEXT, `plannableTodoDate` TEXT, `plannableEndAt` INTEGER, `plannableAllDay` INTEGER, `plannableCourseId` INTEGER, `plannableGroupId` INTEGER, `plannableUserId` INTEGER, `plannableDate` INTEGER NOT NULL, `htmlUrl` TEXT, `submissionStateSubmitted` INTEGER, `submissionStateExcused` INTEGER, `submissionStateGraded` INTEGER, `newActivity` INTEGER, `plannerOverrideId` INTEGER, `plannerOverrideMarkedComplete` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextName", + "columnName": "contextName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableType", + "columnName": "plannableType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "plannableId", + "columnName": "plannableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannableTitle", + "columnName": "plannableTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableDetails", + "columnName": "plannableDetails", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableTodoDate", + "columnName": "plannableTodoDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plannableEndAt", + "columnName": "plannableEndAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableAllDay", + "columnName": "plannableAllDay", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableCourseId", + "columnName": "plannableCourseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableGroupId", + "columnName": "plannableGroupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableUserId", + "columnName": "plannableUserId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannableDate", + "columnName": "plannableDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionStateSubmitted", + "columnName": "submissionStateSubmitted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionStateExcused", + "columnName": "submissionStateExcused", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionStateGraded", + "columnName": "submissionStateGraded", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "newActivity", + "columnName": "newActivity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannerOverrideId", + "columnName": "plannerOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "plannerOverrideMarkedComplete", + "columnName": "plannerOverrideMarkedComplete", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PlannerOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `plannableType` TEXT NOT NULL, `plannableId` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, `markedComplete` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannableType", + "columnName": "plannableType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "plannableId", + "columnName": "plannableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "markedComplete", + "columnName": "markedComplete", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `displayName` TEXT, `fileName` TEXT, `contentType` TEXT, `url` TEXT, `size` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `unlockAt` TEXT, `locked` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `lockAt` TEXT, `hiddenForUser` INTEGER NOT NULL, `thumbnailUrl` TEXT, `modifiedAt` TEXT, `lockedForUser` INTEGER NOT NULL, `previewUrl` TEXT, `lockExplanation` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hiddenForUser", + "columnName": "hiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "modifiedAt", + "columnName": "modifiedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RubricCriterionAssessmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `assignmentId` INTEGER NOT NULL, `ratingId` TEXT, `points` REAL, `comments` TEXT, PRIMARY KEY(`id`, `assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingId", + "columnName": "ratingId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "comments", + "columnName": "comments", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `criterionUseRange` INTEGER NOT NULL, `ignoreForScoring` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "criterionUseRange", + "columnName": "criterionUseRange", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ignoreForScoring", + "columnName": "ignoreForScoring", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionRatingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `rubricCriterionId` TEXT NOT NULL, PRIMARY KEY(`id`, `rubricCriterionId`), FOREIGN KEY(`rubricCriterionId`) REFERENCES `RubricCriterionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rubricCriterionId", + "columnName": "rubricCriterionId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "rubricCriterionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "RubricCriterionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "rubricCriterionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `pointsPossible` REAL NOT NULL, `title` TEXT NOT NULL, `isReusable` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `isReadOnly` INTEGER NOT NULL, `freeFormCriterionComments` INTEGER NOT NULL, `hideScoreTotal` INTEGER NOT NULL, `hidePoints` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isReusable", + "columnName": "isReusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isReadOnly", + "columnName": "isReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hideScoreTotal", + "columnName": "hideScoreTotal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidePoints", + "columnName": "hidePoints", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemAssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentOverrideId` INTEGER NOT NULL, `scheduleItemId` TEXT NOT NULL, PRIMARY KEY(`assignmentOverrideId`, `scheduleItemId`), FOREIGN KEY(`assignmentOverrideId`) REFERENCES `AssignmentOverrideEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`scheduleItemId`) REFERENCES `ScheduleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduleItemId", + "columnName": "scheduleItemId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentOverrideId", + "scheduleItemId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentOverrideEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentOverrideId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "ScheduleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "scheduleItemId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `description` TEXT, `startAt` TEXT, `endAt` TEXT, `isAllDay` INTEGER NOT NULL, `allDayAt` TEXT, `locationAddress` TEXT, `locationName` TEXT, `htmlUrl` TEXT, `contextCode` TEXT, `effectiveContextCode` TEXT, `isHidden` INTEGER NOT NULL, `importantDates` INTEGER NOT NULL, `assignmentId` INTEGER, `type` TEXT NOT NULL, `itemType` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayAt", + "columnName": "allDayAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationAddress", + "columnName": "locationAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationName", + "columnName": "locationName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextCode", + "columnName": "contextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effectiveContextCode", + "columnName": "effectiveContextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "importantDates", + "columnName": "importantDates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemType", + "columnName": "itemType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER, `startAt` TEXT, `endAt` TEXT, `totalStudents` INTEGER NOT NULL, `restrictEnrollmentsToSectionDates` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "totalStudents", + "columnName": "totalStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "restrictEnrollmentsToSectionDates", + "columnName": "restrictEnrollmentsToSectionDates", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionDiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`submissionId` INTEGER NOT NULL, `discussionEntryId` INTEGER NOT NULL, PRIMARY KEY(`submissionId`, `discussionEntryId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "submissionId", + "discussionEntryId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `attempt` INTEGER NOT NULL, `submittedAt` INTEGER, `commentCreated` INTEGER, `mediaContentType` TEXT, `mediaCommentUrl` TEXT, `mediaCommentDisplay` TEXT, `body` TEXT, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, `workflowState` TEXT, `submissionType` TEXT, `previewUrl` TEXT, `url` TEXT, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `mediaCommentId` TEXT, `assignmentId` INTEGER NOT NULL, `userId` INTEGER, `graderId` INTEGER, `groupId` INTEGER, `pointsDeducted` REAL, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `postedAt` INTEGER, `gradingPeriodId` INTEGER, `customGradeStatusId` INTEGER, `hasSubAssignmentSubmissions` INTEGER NOT NULL, PRIMARY KEY(`id`, `attempt`), FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submittedAt", + "columnName": "submittedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "commentCreated", + "columnName": "commentCreated", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaContentType", + "columnName": "mediaContentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentUrl", + "columnName": "mediaCommentUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentDisplay", + "columnName": "mediaCommentDisplay", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionType", + "columnName": "submissionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "graderId", + "columnName": "graderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pointsDeducted", + "columnName": "pointsDeducted", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedAt", + "columnName": "postedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasSubAssignmentSubmissions", + "columnName": "hasSubAssignmentSubmissions", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "attempt" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `autoSyncEnabled` INTEGER NOT NULL, `syncFrequency` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoSyncEnabled", + "columnName": "autoSyncEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncFrequency", + "columnName": "syncFrequency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifiOnly", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TabEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `label` TEXT, `type` TEXT NOT NULL, `htmlUrl` TEXT, `externalUrl` TEXT, `visibility` TEXT NOT NULL, `isHidden` INTEGER NOT NULL, `position` INTEGER NOT NULL, `ltiUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ltiUrl", + "columnName": "ltiUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "TermEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `startAt` TEXT, `endAt` TEXT, `isGroupTerm` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGroupTerm", + "columnName": "isGroupTerm", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserCalendarEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ics` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ics", + "columnName": "ics", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `shortName` TEXT, `loginId` TEXT, `avatarUrl` TEXT, `primaryEmail` TEXT, `email` TEXT, `sortableName` TEXT, `bio` TEXT, `enrollmentIndex` INTEGER NOT NULL, `lastLogin` TEXT, `locale` TEXT, `effective_locale` TEXT, `pronouns` TEXT, `k5User` INTEGER NOT NULL, `rootAccount` TEXT, `isFakeStudent` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginId", + "columnName": "loginId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "primaryEmail", + "columnName": "primaryEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortableName", + "columnName": "sortableName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bio", + "columnName": "bio", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enrollmentIndex", + "columnName": "enrollmentIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastLogin", + "columnName": "lastLogin", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effective_locale", + "columnName": "effective_locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "k5User", + "columnName": "k5User", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rootAccount", + "columnName": "rootAccount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFakeStudent", + "columnName": "isFakeStudent", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "QuizEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `mobileUrl` TEXT, `htmlUrl` TEXT, `description` TEXT, `quizType` TEXT, `assignmentGroupId` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `questionCount` INTEGER NOT NULL, `pointsPossible` TEXT, `isLockQuestionsAfterAnswering` INTEGER NOT NULL, `dueAt` TEXT, `timeLimit` INTEGER NOT NULL, `shuffleAnswers` INTEGER NOT NULL, `showCorrectAnswers` INTEGER NOT NULL, `scoringPolicy` TEXT, `accessCode` TEXT, `ipFilter` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `hideResults` TEXT, `showCorrectAnswersAt` TEXT, `hideCorrectAnswersAt` TEXT, `unlockAt` TEXT, `oneTimeResults` INTEGER NOT NULL, `lockAt` TEXT, `questionTypes` TEXT NOT NULL, `hasAccessCode` INTEGER NOT NULL, `oneQuestionAtATime` INTEGER NOT NULL, `requireLockdownBrowser` INTEGER NOT NULL, `requireLockdownBrowserForResults` INTEGER NOT NULL, `allowAnonymousSubmissions` INTEGER NOT NULL, `published` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `isOnlyVisibleToOverrides` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mobileUrl", + "columnName": "mobileUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizType", + "columnName": "quizType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionCount", + "columnName": "questionCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isLockQuestionsAfterAnswering", + "columnName": "isLockQuestionsAfterAnswering", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeLimit", + "columnName": "timeLimit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shuffleAnswers", + "columnName": "shuffleAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showCorrectAnswers", + "columnName": "showCorrectAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringPolicy", + "columnName": "scoringPolicy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accessCode", + "columnName": "accessCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ipFilter", + "columnName": "ipFilter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideResults", + "columnName": "hideResults", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "showCorrectAnswersAt", + "columnName": "showCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideCorrectAnswersAt", + "columnName": "hideCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oneTimeResults", + "columnName": "oneTimeResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "questionTypes", + "columnName": "questionTypes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasAccessCode", + "columnName": "hasAccessCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneQuestionAtATime", + "columnName": "oneQuestionAtATime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowser", + "columnName": "requireLockdownBrowser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowserForResults", + "columnName": "requireLockdownBrowserForResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowAnonymousSubmissions", + "columnName": "allowAnonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isOnlyVisibleToOverrides", + "columnName": "isOnlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockInfoEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `modulePrerequisiteNames` TEXT, `unlockAt` TEXT, `lockedModuleId` INTEGER, `assignmentId` INTEGER, `moduleId` INTEGER, `pageId` INTEGER, FOREIGN KEY(`moduleId`) REFERENCES `ModuleContentDetailsEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`pageId`) REFERENCES `PageEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "modulePrerequisiteNames", + "columnName": "modulePrerequisiteNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleContentDetailsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "PageEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pageId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockedModuleEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `unlockAt` TEXT, `isRequireSequentialProgress` INTEGER NOT NULL, `lockInfoId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`lockInfoId`) REFERENCES `LockInfoEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isRequireSequentialProgress", + "columnName": "isRequireSequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockInfoId", + "columnName": "lockInfoId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockInfoEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockInfoId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleNameEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `lockedModuleId` INTEGER NOT NULL, FOREIGN KEY(`lockedModuleId`) REFERENCES `LockedModuleEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockedModuleEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockedModuleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleCompletionRequirementEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT, `minScore` REAL NOT NULL, `maxScore` REAL NOT NULL, `completed` INTEGER, `moduleId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "minScore", + "columnName": "minScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxScore", + "columnName": "maxScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `fileName` TEXT, `courseId` INTEGER NOT NULL, `url` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "ConferenceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `conferenceKey` TEXT, `conferenceType` TEXT, `description` TEXT, `duration` INTEGER NOT NULL, `endedAt` INTEGER, `hasAdvancedSettings` INTEGER NOT NULL, `joinUrl` TEXT, `longRunning` INTEGER NOT NULL, `startedAt` INTEGER, `title` TEXT, `url` TEXT, `contextType` TEXT NOT NULL, `contextId` INTEGER NOT NULL, `record` INTEGER, `users` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conferenceKey", + "columnName": "conferenceKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "conferenceType", + "columnName": "conferenceType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAdvancedSettings", + "columnName": "hasAdvancedSettings", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinUrl", + "columnName": "joinUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longRunning", + "columnName": "longRunning", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startedAt", + "columnName": "startedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "record", + "columnName": "record", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "users", + "columnName": "users", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ConferenceRecordingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`recordingId` TEXT NOT NULL, `conferenceId` INTEGER NOT NULL, `createdAtMillis` INTEGER NOT NULL, `durationMinutes` INTEGER NOT NULL, `playbackUrl` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`recordingId`), FOREIGN KEY(`conferenceId`) REFERENCES `ConferenceEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "recordingId", + "columnName": "recordingId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conferenceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAtMillis", + "columnName": "createdAtMillis", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durationMinutes", + "columnName": "durationMinutes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playbackUrl", + "columnName": "playbackUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "recordingId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ConferenceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "conferenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFeaturesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `features` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contentType` TEXT, `filename` TEXT, `displayName` TEXT, `url` TEXT, `thumbnailUrl` TEXT, `previewUrl` TEXT, `createdAt` INTEGER, `size` INTEGER NOT NULL, `workerId` TEXT, `submissionCommentId` INTEGER, `submissionId` INTEGER, `attempt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionCommentId`) REFERENCES `SubmissionCommentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionCommentId", + "columnName": "submissionCommentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionCommentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionCommentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MediaCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mediaId` TEXT NOT NULL, `submissionId` INTEGER NOT NULL, `attemptId` INTEGER NOT NULL, `displayName` TEXT, `url` TEXT, `mediaType` TEXT, `contentType` TEXT, PRIMARY KEY(`mediaId`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "mediaId", + "columnName": "mediaId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaType", + "columnName": "mediaType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "mediaId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "AuthorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, `pronouns` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `authorId` INTEGER NOT NULL, `authorName` TEXT, `authorPronouns` TEXT, `comment` TEXT, `createdAt` INTEGER, `mediaCommentId` TEXT, `attemptId` INTEGER, `submissionId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorName", + "columnName": "authorName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorPronouns", + "columnName": "authorPronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "DiscussionTopicEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unreadEntries` TEXT NOT NULL, `participantIds` TEXT NOT NULL, `viewIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadEntries", + "columnName": "unreadEntries", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantIds", + "columnName": "participantIds", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viewIds", + "columnName": "viewIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CourseSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `tabs` TEXT NOT NULL, `additionalFilesStarted` INTEGER NOT NULL, `progressState` TEXT NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "additionalFilesStarted", + "columnName": "additionalFilesStarted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FileSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `additionalFile` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseSyncProgressEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "fileId", + "columnName": "fileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "additionalFile", + "columnName": "additionalFile", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncProgressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "StudioMediaProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ltiLaunchId` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "ltiLaunchId", + "columnName": "ltiLaunchId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CustomGradeStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CheckpointEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `assignmentId` INTEGER, `name` TEXT, `tag` TEXT, `pointsPossible` REAL, `dueAt` TEXT, `onlyVisibleToOverrides` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, `moduleItemId` INTEGER, `courseId` INTEGER, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`moduleItemId`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "moduleItemId", + "columnName": "moduleItemId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleItemId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubAssignmentSubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `submissionId` INTEGER NOT NULL, `submissionAttempt` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `latePolicyStatus` TEXT, `customGradeStatusId` INTEGER, `subAssignmentTag` TEXT, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `userId` INTEGER NOT NULL, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, FOREIGN KEY(`submissionId`, `submissionAttempt`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionAttempt", + "columnName": "submissionAttempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latePolicyStatus", + "columnName": "latePolicyStatus", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subAssignmentTag", + "columnName": "subAssignmentTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "submissionAttempt" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '371b12b510101fec6ff4b7155ac8b092')" + ] + } +} \ No newline at end of file diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/9.json b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/9.json new file mode 100644 index 0000000000..2bb71c37bc --- /dev/null +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/9.json @@ -0,0 +1,5618 @@ +{ + "formatVersion": 1, + "database": { + "version": 9, + "identityHash": "3bcbb0a766401a1a19d753efa8b89959", + "entities": [ + { + "tableName": "AssignmentDueDateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `assignmentOverrideId` INTEGER, `dueAt` TEXT, `title` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `isBase` INTEGER NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER" + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT" + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT" + }, + { + "fieldPath": "isBase", + "columnName": "isBase", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `submissionTypesRaw` TEXT NOT NULL, `dueAt` TEXT, `pointsPossible` REAL NOT NULL, `courseId` INTEGER NOT NULL, `isGradeGroupsIndividually` INTEGER NOT NULL, `gradingType` TEXT, `needsGradingCount` INTEGER NOT NULL, `htmlUrl` TEXT, `url` TEXT, `quizId` INTEGER NOT NULL, `isUseRubricForGrading` INTEGER NOT NULL, `rubricSettingsId` INTEGER, `allowedExtensions` TEXT NOT NULL, `submissionId` INTEGER, `assignmentGroupId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `isPeerReviews` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, `lockExplanation` TEXT, `discussionTopicHeaderId` INTEGER, `freeFormCriterionComments` INTEGER NOT NULL, `published` INTEGER NOT NULL, `groupCategoryId` INTEGER NOT NULL, `userSubmitted` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `onlyVisibleToOverrides` INTEGER NOT NULL, `anonymousPeerReviews` INTEGER NOT NULL, `moderatedGrading` INTEGER NOT NULL, `anonymousGrading` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `plannerOverrideId` INTEGER, `isStudioEnabled` INTEGER NOT NULL, `inClosedGradingPeriod` INTEGER NOT NULL, `annotatableAttachmentId` INTEGER NOT NULL, `anonymousSubmissions` INTEGER NOT NULL, `omitFromFinalGrade` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentGroupId`) REFERENCES `AssignmentGroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "submissionTypesRaw", + "columnName": "submissionTypesRaw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT" + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeGroupsIndividually", + "columnName": "isGradeGroupsIndividually", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingType", + "columnName": "gradingType", + "affinity": "TEXT" + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "quizId", + "columnName": "quizId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUseRubricForGrading", + "columnName": "isUseRubricForGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricSettingsId", + "columnName": "rubricSettingsId", + "affinity": "INTEGER" + }, + { + "fieldPath": "allowedExtensions", + "columnName": "allowedExtensions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER" + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPeerReviews", + "columnName": "isPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT" + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT" + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT" + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER" + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userSubmitted", + "columnName": "userSubmitted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousPeerReviews", + "columnName": "anonymousPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moderatedGrading", + "columnName": "moderatedGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousGrading", + "columnName": "anonymousGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannerOverrideId", + "columnName": "plannerOverrideId", + "affinity": "INTEGER" + }, + { + "fieldPath": "isStudioEnabled", + "columnName": "isStudioEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inClosedGradingPeriod", + "columnName": "inClosedGradingPeriod", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "annotatableAttachmentId", + "columnName": "annotatableAttachmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousSubmissions", + "columnName": "anonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "omitFromFinalGrade", + "columnName": "omitFromFinalGrade", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "AssignmentGroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentGroupId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentGroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `position` INTEGER NOT NULL, `groupWeight` REAL NOT NULL, `rules` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupWeight", + "columnName": "groupWeight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rules", + "columnName": "rules", + "affinity": "TEXT" + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `title` TEXT, `dueAt` INTEGER, `isAllDay` INTEGER NOT NULL, `allDayDate` TEXT, `unlockAt` INTEGER, `lockAt` INTEGER, `courseSectionId` INTEGER NOT NULL, `groupId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayDate", + "columnName": "allDayDate", + "affinity": "TEXT" + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentRubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `rubricId` TEXT NOT NULL, PRIMARY KEY(`assignmentId`, `rubricId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricId", + "columnName": "rubricId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId", + "rubricId" + ] + }, + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentScoreStatisticsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `mean` REAL NOT NULL, `min` REAL NOT NULL, `max` REAL NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mean", + "columnName": "mean", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "min", + "columnName": "min", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "max", + "columnName": "max", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentSetEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `scoringRangeId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `position` INTEGER NOT NULL, `masteryPathId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`masteryPathId`) REFERENCES `MasteryPathEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringRangeId", + "columnName": "scoringRangeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT" + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT" + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "masteryPathId", + "columnName": "masteryPathId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "MasteryPathEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "masteryPathId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `originalName` TEXT, `courseCode` TEXT, `startAt` TEXT, `endAt` TEXT, `syllabusBody` TEXT, `hideFinalGrades` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `license` TEXT NOT NULL, `termId` INTEGER, `needsGradingCount` INTEGER NOT NULL, `isApplyAssignmentGroupWeights` INTEGER NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, `isFavorite` INTEGER NOT NULL, `accessRestrictedByDate` INTEGER NOT NULL, `imageUrl` TEXT, `bannerImageUrl` TEXT, `isWeightedGradingPeriods` INTEGER NOT NULL, `hasGradingPeriods` INTEGER NOT NULL, `homePage` TEXT, `restrictEnrollmentsToCourseDate` INTEGER NOT NULL, `workflowState` TEXT, `homeroomCourse` INTEGER NOT NULL, `courseColor` TEXT, `gradingScheme` TEXT, `pointsBasedGradingScheme` INTEGER NOT NULL, `scalingFactor` REAL NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`termId`) REFERENCES `TermEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT" + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT" + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT" + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT" + }, + { + "fieldPath": "syllabusBody", + "columnName": "syllabusBody", + "affinity": "TEXT" + }, + { + "fieldPath": "hideFinalGrades", + "columnName": "hideFinalGrades", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "license", + "columnName": "license", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "termId", + "columnName": "termId", + "affinity": "INTEGER" + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isApplyAssignmentGroupWeights", + "columnName": "isApplyAssignmentGroupWeights", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL" + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL" + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT" + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT" + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessRestrictedByDate", + "columnName": "accessRestrictedByDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imageUrl", + "columnName": "imageUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "bannerImageUrl", + "columnName": "bannerImageUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "isWeightedGradingPeriods", + "columnName": "isWeightedGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasGradingPeriods", + "columnName": "hasGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homePage", + "columnName": "homePage", + "affinity": "TEXT" + }, + { + "fieldPath": "restrictEnrollmentsToCourseDate", + "columnName": "restrictEnrollmentsToCourseDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT" + }, + { + "fieldPath": "homeroomCourse", + "columnName": "homeroomCourse", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseColor", + "columnName": "courseColor", + "affinity": "TEXT" + }, + { + "fieldPath": "gradingScheme", + "columnName": "gradingScheme", + "affinity": "TEXT" + }, + { + "fieldPath": "pointsBasedGradingScheme", + "columnName": "pointsBasedGradingScheme", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scalingFactor", + "columnName": "scalingFactor", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "TermEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "termId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFilesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`courseId`, `url`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "url" + ] + }, + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "CourseGradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `gradingPeriodId` INTEGER NOT NULL, PRIMARY KEY(`courseId`, `gradingPeriodId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`gradingPeriodId`) REFERENCES `GradingPeriodEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "gradingPeriodId" + ] + }, + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "GradingPeriodEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "gradingPeriodId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseSummary` INTEGER, `restrictQuantitativeData` INTEGER NOT NULL, PRIMARY KEY(`courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseSummary", + "columnName": "courseSummary", + "affinity": "INTEGER" + }, + { + "fieldPath": "restrictQuantitativeData", + "columnName": "restrictQuantitativeData", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `fullContentSync` INTEGER NOT NULL, `tabs` TEXT NOT NULL, `fullFileSync` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullContentSync", + "columnName": "fullContentSync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullFileSync", + "columnName": "fullFileSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + } + }, + { + "tableName": "DashboardCardEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isK5Subject` INTEGER NOT NULL, `shortName` TEXT, `originalName` TEXT, `courseCode` TEXT, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isK5Subject", + "columnName": "isK5Subject", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT" + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT" + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT" + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "DiscussionEntryAttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionEntryId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionEntryId`, `remoteFileId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionEntryId", + "remoteFileId" + ] + }, + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `updatedAt` TEXT, `createdAt` TEXT, `authorId` INTEGER, `description` TEXT, `userId` INTEGER NOT NULL, `parentId` INTEGER NOT NULL, `message` TEXT, `deleted` INTEGER NOT NULL, `totalChildren` INTEGER NOT NULL, `unreadChildren` INTEGER NOT NULL, `ratingCount` INTEGER NOT NULL, `ratingSum` INTEGER NOT NULL, `editorId` INTEGER NOT NULL, `_hasRated` INTEGER NOT NULL, `replyIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT" + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT" + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChildren", + "columnName": "totalChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadChildren", + "columnName": "unreadChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingCount", + "columnName": "ratingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingSum", + "columnName": "ratingSum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editorId", + "columnName": "editorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "_hasRated", + "columnName": "_hasRated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyIds", + "columnName": "replyIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "DiscussionParticipantEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `pronouns` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT" + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "DiscussionTopicHeaderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `discussionType` TEXT, `title` TEXT, `message` TEXT, `htmlUrl` TEXT, `postedDate` INTEGER, `delayedPostDate` INTEGER, `lastReplyDate` INTEGER, `requireInitialPost` INTEGER NOT NULL, `discussionSubentryCount` INTEGER NOT NULL, `readState` TEXT, `unreadCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `assignmentId` INTEGER, `locked` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `pinned` INTEGER NOT NULL, `authorId` INTEGER, `podcastUrl` TEXT, `groupCategoryId` TEXT, `announcement` INTEGER NOT NULL, `permissionId` INTEGER, `published` INTEGER NOT NULL, `allowRating` INTEGER NOT NULL, `onlyGradersCanRate` INTEGER NOT NULL, `sortByRating` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `lockAt` INTEGER, `userCanSeePosts` INTEGER NOT NULL, `specificSections` TEXT, `anonymousState` TEXT, `replyRequiredCount` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`authorId`) REFERENCES `DiscussionParticipantEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`permissionId`) REFERENCES `DiscussionTopicPermissionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionType", + "columnName": "discussionType", + "affinity": "TEXT" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT" + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "postedDate", + "columnName": "postedDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "delayedPostDate", + "columnName": "delayedPostDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastReplyDate", + "columnName": "lastReplyDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "requireInitialPost", + "columnName": "requireInitialPost", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionSubentryCount", + "columnName": "discussionSubentryCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readState", + "columnName": "readState", + "affinity": "TEXT" + }, + { + "fieldPath": "unreadCount", + "columnName": "unreadCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER" + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT" + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER" + }, + { + "fieldPath": "podcastUrl", + "columnName": "podcastUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "TEXT" + }, + { + "fieldPath": "announcement", + "columnName": "announcement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "permissionId", + "columnName": "permissionId", + "affinity": "INTEGER" + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowRating", + "columnName": "allowRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyGradersCanRate", + "columnName": "onlyGradersCanRate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortByRating", + "columnName": "sortByRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "userCanSeePosts", + "columnName": "userCanSeePosts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "specificSections", + "columnName": "specificSections", + "affinity": "TEXT" + }, + { + "fieldPath": "anonymousState", + "columnName": "anonymousState", + "affinity": "TEXT" + }, + { + "fieldPath": "replyRequiredCount", + "columnName": "replyRequiredCount", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "DiscussionParticipantEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "authorId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "DiscussionTopicPermissionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "permissionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicPermissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `discussionTopicHeaderId` INTEGER NOT NULL, `attach` INTEGER NOT NULL, `update` INTEGER NOT NULL, `delete` INTEGER NOT NULL, `reply` INTEGER NOT NULL, FOREIGN KEY(`discussionTopicHeaderId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attach", + "columnName": "attach", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delete", + "columnName": "delete", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reply", + "columnName": "reply", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicHeaderId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicRemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionId`, `remoteFileId`), FOREIGN KEY(`discussionId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionId", + "columnName": "discussionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionId", + "remoteFileId" + ] + }, + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicSectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionTopicId` INTEGER NOT NULL, `sectionId` INTEGER NOT NULL, PRIMARY KEY(`discussionTopicId`, `sectionId`), FOREIGN KEY(`discussionTopicId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionTopicId", + "columnName": "discussionTopicId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionTopicId", + "sectionId" + ] + }, + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "EnrollmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `role` TEXT NOT NULL, `type` TEXT NOT NULL, `courseId` INTEGER, `courseSectionId` INTEGER, `enrollmentState` TEXT, `userId` INTEGER NOT NULL, `computedCurrentScore` REAL, `computedFinalScore` REAL, `computedCurrentGrade` TEXT, `computedFinalGrade` TEXT, `multipleGradingPeriodsEnabled` INTEGER NOT NULL, `totalsForAllGradingPeriodsOption` INTEGER NOT NULL, `currentPeriodComputedCurrentScore` REAL, `currentPeriodComputedFinalScore` REAL, `currentPeriodComputedCurrentGrade` TEXT, `currentPeriodComputedFinalGrade` TEXT, `currentGradingPeriodId` INTEGER NOT NULL, `currentGradingPeriodTitle` TEXT, `associatedUserId` INTEGER NOT NULL, `lastActivityAt` INTEGER, `limitPrivilegesToCourseSection` INTEGER NOT NULL, `observedUserId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`observedUserId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseSectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER" + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER" + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT" + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "computedCurrentScore", + "columnName": "computedCurrentScore", + "affinity": "REAL" + }, + { + "fieldPath": "computedFinalScore", + "columnName": "computedFinalScore", + "affinity": "REAL" + }, + { + "fieldPath": "computedCurrentGrade", + "columnName": "computedCurrentGrade", + "affinity": "TEXT" + }, + { + "fieldPath": "computedFinalGrade", + "columnName": "computedFinalGrade", + "affinity": "TEXT" + }, + { + "fieldPath": "multipleGradingPeriodsEnabled", + "columnName": "multipleGradingPeriodsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalsForAllGradingPeriodsOption", + "columnName": "totalsForAllGradingPeriodsOption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPeriodComputedCurrentScore", + "columnName": "currentPeriodComputedCurrentScore", + "affinity": "REAL" + }, + { + "fieldPath": "currentPeriodComputedFinalScore", + "columnName": "currentPeriodComputedFinalScore", + "affinity": "REAL" + }, + { + "fieldPath": "currentPeriodComputedCurrentGrade", + "columnName": "currentPeriodComputedCurrentGrade", + "affinity": "TEXT" + }, + { + "fieldPath": "currentPeriodComputedFinalGrade", + "columnName": "currentPeriodComputedFinalGrade", + "affinity": "TEXT" + }, + { + "fieldPath": "currentGradingPeriodId", + "columnName": "currentGradingPeriodId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentGradingPeriodTitle", + "columnName": "currentGradingPeriodTitle", + "affinity": "TEXT" + }, + { + "fieldPath": "associatedUserId", + "columnName": "associatedUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivityAt", + "columnName": "lastActivityAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "limitPrivilegesToCourseSection", + "columnName": "limitPrivilegesToCourseSection", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "observedUserId", + "columnName": "observedUserId", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "observedUserId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "courseSectionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileFolderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `createdDate` INTEGER, `updatedDate` INTEGER, `unlockDate` INTEGER, `lockDate` INTEGER, `isLocked` INTEGER NOT NULL, `isHidden` INTEGER NOT NULL, `isLockedForUser` INTEGER NOT NULL, `isHiddenForUser` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `size` INTEGER NOT NULL, `contentType` TEXT, `url` TEXT, `displayName` TEXT, `thumbnailUrl` TEXT, `parentFolderId` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `filesCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `foldersCount` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `foldersUrl` TEXT, `filesUrl` TEXT, `fullName` TEXT, `forSubmissions` INTEGER NOT NULL, `canUpload` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "updatedDate", + "columnName": "updatedDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "unlockDate", + "columnName": "unlockDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockDate", + "columnName": "lockDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLockedForUser", + "columnName": "isLockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHiddenForUser", + "columnName": "isHiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "parentFolderId", + "columnName": "parentFolderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filesCount", + "columnName": "filesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "foldersCount", + "columnName": "foldersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "foldersUrl", + "columnName": "foldersUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "filesUrl", + "columnName": "filesUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT" + }, + { + "fieldPath": "forSubmissions", + "columnName": "forSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canUpload", + "columnName": "canUpload", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "EditDashboardItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `name` TEXT NOT NULL, `isFavorite` INTEGER NOT NULL, `enrollmentState` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + } + }, + { + "tableName": "ExternalToolAttributesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `url` TEXT, `newTab` INTEGER NOT NULL, `resourceLinkid` TEXT, `contentId` INTEGER, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "newTab", + "columnName": "newTab", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceLinkid", + "columnName": "resourceLinkid", + "affinity": "TEXT" + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`enrollmentId` INTEGER NOT NULL, `htmlUrl` TEXT NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, PRIMARY KEY(`enrollmentId`), FOREIGN KEY(`enrollmentId`) REFERENCES `EnrollmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "enrollmentId", + "columnName": "enrollmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL" + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL" + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT" + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "enrollmentId" + ] + }, + "foreignKeys": [ + { + "table": "EnrollmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "enrollmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `startDate` TEXT, `endDate` TEXT, `weight` REAL NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "TEXT" + }, + { + "fieldPath": "endDate", + "columnName": "endDate", + "affinity": "TEXT" + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "GroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `avatarUrl` TEXT, `isPublic` INTEGER NOT NULL, `membersCount` INTEGER NOT NULL, `joinLevel` TEXT, `courseId` INTEGER NOT NULL, `accountId` INTEGER NOT NULL, `role` TEXT, `groupCategoryId` INTEGER NOT NULL, `storageQuotaMb` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `concluded` INTEGER NOT NULL, `canAccess` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "membersCount", + "columnName": "membersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinLevel", + "columnName": "joinLevel", + "affinity": "TEXT" + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT" + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "storageQuotaMb", + "columnName": "storageQuotaMb", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concluded", + "columnName": "concluded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canAccess", + "columnName": "canAccess", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "GroupUserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LocalFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `createdDate` INTEGER NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "MasteryPathAssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `overrideId` INTEGER NOT NULL, `assignmentSetId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentSetId`) REFERENCES `AssignmentSetEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT" + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT" + }, + { + "fieldPath": "overrideId", + "columnName": "overrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentSetId", + "columnName": "assignmentSetId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "AssignmentSetEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentSetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MasteryPathEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `selectedSetId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectedSetId", + "columnName": "selectedSetId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleContentDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `pointsPossible` TEXT, `dueAt` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT" + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT" + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT" + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT" + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `moduleId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `title` TEXT, `indent` INTEGER NOT NULL, `type` TEXT, `htmlUrl` TEXT, `url` TEXT, `published` INTEGER, `contentId` INTEGER NOT NULL, `externalUrl` TEXT, `pageUrl` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`moduleId`) REFERENCES `ModuleObjectEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "indent", + "columnName": "indent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER" + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "pageUrl", + "columnName": "pageUrl", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "ModuleObjectEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleObjectEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `name` TEXT, `unlockAt` TEXT, `sequentialProgress` INTEGER NOT NULL, `prerequisiteIds` TEXT, `state` TEXT, `completedAt` TEXT, `published` INTEGER, `itemCount` INTEGER NOT NULL, `itemsUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT" + }, + { + "fieldPath": "sequentialProgress", + "columnName": "sequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prerequisiteIds", + "columnName": "prerequisiteIds", + "affinity": "TEXT" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT" + }, + { + "fieldPath": "completedAt", + "columnName": "completedAt", + "affinity": "TEXT" + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER" + }, + { + "fieldPath": "itemCount", + "columnName": "itemCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemsUrl", + "columnName": "itemsUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "NeedsGradingCountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sectionId` INTEGER NOT NULL, `needsGradingCount` INTEGER NOT NULL, PRIMARY KEY(`sectionId`), FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sectionId" + ] + }, + "foreignKeys": [ + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `createdAt` INTEGER, `updatedAt` INTEGER, `hideFromStudents` INTEGER NOT NULL, `status` TEXT, `body` TEXT, `frontPage` INTEGER NOT NULL, `published` INTEGER NOT NULL, `editingRoles` TEXT, `htmlUrl` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "hideFromStudents", + "columnName": "hideFromStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT" + }, + { + "fieldPath": "frontPage", + "columnName": "frontPage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editingRoles", + "columnName": "editingRoles", + "affinity": "TEXT" + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PlannerItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER, `groupId` INTEGER, `userId` INTEGER, `contextType` TEXT, `contextName` TEXT, `plannableType` TEXT NOT NULL, `plannableId` INTEGER NOT NULL, `plannableTitle` TEXT, `plannableDetails` TEXT, `plannableTodoDate` TEXT, `plannableEndAt` INTEGER, `plannableAllDay` INTEGER, `plannableCourseId` INTEGER, `plannableGroupId` INTEGER, `plannableUserId` INTEGER, `plannableDate` INTEGER NOT NULL, `htmlUrl` TEXT, `submissionStateSubmitted` INTEGER, `submissionStateExcused` INTEGER, `submissionStateGraded` INTEGER, `newActivity` INTEGER, `plannerOverrideId` INTEGER, `plannerOverrideMarkedComplete` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER" + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER" + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER" + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT" + }, + { + "fieldPath": "contextName", + "columnName": "contextName", + "affinity": "TEXT" + }, + { + "fieldPath": "plannableType", + "columnName": "plannableType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "plannableId", + "columnName": "plannableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannableTitle", + "columnName": "plannableTitle", + "affinity": "TEXT" + }, + { + "fieldPath": "plannableDetails", + "columnName": "plannableDetails", + "affinity": "TEXT" + }, + { + "fieldPath": "plannableTodoDate", + "columnName": "plannableTodoDate", + "affinity": "TEXT" + }, + { + "fieldPath": "plannableEndAt", + "columnName": "plannableEndAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "plannableAllDay", + "columnName": "plannableAllDay", + "affinity": "INTEGER" + }, + { + "fieldPath": "plannableCourseId", + "columnName": "plannableCourseId", + "affinity": "INTEGER" + }, + { + "fieldPath": "plannableGroupId", + "columnName": "plannableGroupId", + "affinity": "INTEGER" + }, + { + "fieldPath": "plannableUserId", + "columnName": "plannableUserId", + "affinity": "INTEGER" + }, + { + "fieldPath": "plannableDate", + "columnName": "plannableDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "submissionStateSubmitted", + "columnName": "submissionStateSubmitted", + "affinity": "INTEGER" + }, + { + "fieldPath": "submissionStateExcused", + "columnName": "submissionStateExcused", + "affinity": "INTEGER" + }, + { + "fieldPath": "submissionStateGraded", + "columnName": "submissionStateGraded", + "affinity": "INTEGER" + }, + { + "fieldPath": "newActivity", + "columnName": "newActivity", + "affinity": "INTEGER" + }, + { + "fieldPath": "plannerOverrideId", + "columnName": "plannerOverrideId", + "affinity": "INTEGER" + }, + { + "fieldPath": "plannerOverrideMarkedComplete", + "columnName": "plannerOverrideMarkedComplete", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PlannerOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `plannableType` TEXT NOT NULL, `plannableId` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, `markedComplete` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannableType", + "columnName": "plannableType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "plannableId", + "columnName": "plannableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "markedComplete", + "columnName": "markedComplete", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "RemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `displayName` TEXT, `fileName` TEXT, `contentType` TEXT, `url` TEXT, `size` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `unlockAt` TEXT, `locked` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `lockAt` TEXT, `hiddenForUser` INTEGER NOT NULL, `thumbnailUrl` TEXT, `modifiedAt` TEXT, `lockedForUser` INTEGER NOT NULL, `previewUrl` TEXT, `lockExplanation` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT" + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT" + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT" + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT" + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT" + }, + { + "fieldPath": "hiddenForUser", + "columnName": "hiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "modifiedAt", + "columnName": "modifiedAt", + "affinity": "TEXT" + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "RubricCriterionAssessmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `assignmentId` INTEGER NOT NULL, `ratingId` TEXT, `points` REAL, `comments` TEXT, PRIMARY KEY(`id`, `assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingId", + "columnName": "ratingId", + "affinity": "TEXT" + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL" + }, + { + "fieldPath": "comments", + "columnName": "comments", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "assignmentId" + ] + }, + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `criterionUseRange` INTEGER NOT NULL, `ignoreForScoring` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "criterionUseRange", + "columnName": "criterionUseRange", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ignoreForScoring", + "columnName": "ignoreForScoring", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionRatingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `rubricCriterionId` TEXT NOT NULL, PRIMARY KEY(`id`, `rubricCriterionId`), FOREIGN KEY(`rubricCriterionId`) REFERENCES `RubricCriterionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rubricCriterionId", + "columnName": "rubricCriterionId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "rubricCriterionId" + ] + }, + "foreignKeys": [ + { + "table": "RubricCriterionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "rubricCriterionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `pointsPossible` REAL NOT NULL, `title` TEXT NOT NULL, `isReusable` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `isReadOnly` INTEGER NOT NULL, `freeFormCriterionComments` INTEGER NOT NULL, `hideScoreTotal` INTEGER NOT NULL, `hidePoints` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT" + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isReusable", + "columnName": "isReusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isReadOnly", + "columnName": "isReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hideScoreTotal", + "columnName": "hideScoreTotal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidePoints", + "columnName": "hidePoints", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemAssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentOverrideId` INTEGER NOT NULL, `scheduleItemId` TEXT NOT NULL, PRIMARY KEY(`assignmentOverrideId`, `scheduleItemId`), FOREIGN KEY(`assignmentOverrideId`) REFERENCES `AssignmentOverrideEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`scheduleItemId`) REFERENCES `ScheduleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduleItemId", + "columnName": "scheduleItemId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentOverrideId", + "scheduleItemId" + ] + }, + "foreignKeys": [ + { + "table": "AssignmentOverrideEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentOverrideId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "ScheduleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "scheduleItemId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `description` TEXT, `startAt` TEXT, `endAt` TEXT, `isAllDay` INTEGER NOT NULL, `allDayAt` TEXT, `locationAddress` TEXT, `locationName` TEXT, `htmlUrl` TEXT, `contextCode` TEXT, `effectiveContextCode` TEXT, `isHidden` INTEGER NOT NULL, `importantDates` INTEGER NOT NULL, `assignmentId` INTEGER, `subAssignmentId` INTEGER, `type` TEXT NOT NULL, `itemType` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT" + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT" + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayAt", + "columnName": "allDayAt", + "affinity": "TEXT" + }, + { + "fieldPath": "locationAddress", + "columnName": "locationAddress", + "affinity": "TEXT" + }, + { + "fieldPath": "locationName", + "columnName": "locationName", + "affinity": "TEXT" + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "contextCode", + "columnName": "contextCode", + "affinity": "TEXT" + }, + { + "fieldPath": "effectiveContextCode", + "columnName": "effectiveContextCode", + "affinity": "TEXT" + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "importantDates", + "columnName": "importantDates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER" + }, + { + "fieldPath": "subAssignmentId", + "columnName": "subAssignmentId", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemType", + "columnName": "itemType", + "affinity": "TEXT" + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER, `startAt` TEXT, `endAt` TEXT, `totalStudents` INTEGER NOT NULL, `restrictEnrollmentsToSectionDates` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER" + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT" + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT" + }, + { + "fieldPath": "totalStudents", + "columnName": "totalStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "restrictEnrollmentsToSectionDates", + "columnName": "restrictEnrollmentsToSectionDates", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionDiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`submissionId` INTEGER NOT NULL, `discussionEntryId` INTEGER NOT NULL, PRIMARY KEY(`submissionId`, `discussionEntryId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "submissionId", + "discussionEntryId" + ] + }, + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `attempt` INTEGER NOT NULL, `submittedAt` INTEGER, `commentCreated` INTEGER, `mediaContentType` TEXT, `mediaCommentUrl` TEXT, `mediaCommentDisplay` TEXT, `body` TEXT, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, `workflowState` TEXT, `submissionType` TEXT, `previewUrl` TEXT, `url` TEXT, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `mediaCommentId` TEXT, `assignmentId` INTEGER NOT NULL, `userId` INTEGER, `graderId` INTEGER, `groupId` INTEGER, `pointsDeducted` REAL, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `postedAt` INTEGER, `gradingPeriodId` INTEGER, `customGradeStatusId` INTEGER, `hasSubAssignmentSubmissions` INTEGER NOT NULL, PRIMARY KEY(`id`, `attempt`), FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT" + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submittedAt", + "columnName": "submittedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "commentCreated", + "columnName": "commentCreated", + "affinity": "INTEGER" + }, + { + "fieldPath": "mediaContentType", + "columnName": "mediaContentType", + "affinity": "TEXT" + }, + { + "fieldPath": "mediaCommentUrl", + "columnName": "mediaCommentUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "mediaCommentDisplay", + "columnName": "mediaCommentDisplay", + "affinity": "TEXT" + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT" + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT" + }, + { + "fieldPath": "submissionType", + "columnName": "submissionType", + "affinity": "TEXT" + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT" + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER" + }, + { + "fieldPath": "graderId", + "columnName": "graderId", + "affinity": "INTEGER" + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER" + }, + { + "fieldPath": "pointsDeducted", + "columnName": "pointsDeducted", + "affinity": "REAL" + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT" + }, + { + "fieldPath": "postedAt", + "columnName": "postedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER" + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasSubAssignmentSubmissions", + "columnName": "hasSubAssignmentSubmissions", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "attempt" + ] + }, + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `autoSyncEnabled` INTEGER NOT NULL, `syncFrequency` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoSyncEnabled", + "columnName": "autoSyncEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncFrequency", + "columnName": "syncFrequency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifiOnly", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "TabEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `label` TEXT, `type` TEXT NOT NULL, `htmlUrl` TEXT, `externalUrl` TEXT, `visibility` TEXT NOT NULL, `isHidden` INTEGER NOT NULL, `position` INTEGER NOT NULL, `ltiUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ltiUrl", + "columnName": "ltiUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "TermEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `startAt` TEXT, `endAt` TEXT, `isGroupTerm` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT" + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT" + }, + { + "fieldPath": "isGroupTerm", + "columnName": "isGroupTerm", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "UserCalendarEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ics` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ics", + "columnName": "ics", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "UserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `shortName` TEXT, `loginId` TEXT, `avatarUrl` TEXT, `primaryEmail` TEXT, `email` TEXT, `sortableName` TEXT, `bio` TEXT, `enrollmentIndex` INTEGER NOT NULL, `lastLogin` TEXT, `locale` TEXT, `effective_locale` TEXT, `pronouns` TEXT, `k5User` INTEGER NOT NULL, `rootAccount` TEXT, `isFakeStudent` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT" + }, + { + "fieldPath": "loginId", + "columnName": "loginId", + "affinity": "TEXT" + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "primaryEmail", + "columnName": "primaryEmail", + "affinity": "TEXT" + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT" + }, + { + "fieldPath": "sortableName", + "columnName": "sortableName", + "affinity": "TEXT" + }, + { + "fieldPath": "bio", + "columnName": "bio", + "affinity": "TEXT" + }, + { + "fieldPath": "enrollmentIndex", + "columnName": "enrollmentIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastLogin", + "columnName": "lastLogin", + "affinity": "TEXT" + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT" + }, + { + "fieldPath": "effective_locale", + "columnName": "effective_locale", + "affinity": "TEXT" + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT" + }, + { + "fieldPath": "k5User", + "columnName": "k5User", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rootAccount", + "columnName": "rootAccount", + "affinity": "TEXT" + }, + { + "fieldPath": "isFakeStudent", + "columnName": "isFakeStudent", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "QuizEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `mobileUrl` TEXT, `htmlUrl` TEXT, `description` TEXT, `quizType` TEXT, `assignmentGroupId` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `questionCount` INTEGER NOT NULL, `pointsPossible` TEXT, `isLockQuestionsAfterAnswering` INTEGER NOT NULL, `dueAt` TEXT, `timeLimit` INTEGER NOT NULL, `shuffleAnswers` INTEGER NOT NULL, `showCorrectAnswers` INTEGER NOT NULL, `scoringPolicy` TEXT, `accessCode` TEXT, `ipFilter` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `hideResults` TEXT, `showCorrectAnswersAt` TEXT, `hideCorrectAnswersAt` TEXT, `unlockAt` TEXT, `oneTimeResults` INTEGER NOT NULL, `lockAt` TEXT, `questionTypes` TEXT NOT NULL, `hasAccessCode` INTEGER NOT NULL, `oneQuestionAtATime` INTEGER NOT NULL, `requireLockdownBrowser` INTEGER NOT NULL, `requireLockdownBrowserForResults` INTEGER NOT NULL, `allowAnonymousSubmissions` INTEGER NOT NULL, `published` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `isOnlyVisibleToOverrides` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "mobileUrl", + "columnName": "mobileUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "quizType", + "columnName": "quizType", + "affinity": "TEXT" + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionCount", + "columnName": "questionCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT" + }, + { + "fieldPath": "isLockQuestionsAfterAnswering", + "columnName": "isLockQuestionsAfterAnswering", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT" + }, + { + "fieldPath": "timeLimit", + "columnName": "timeLimit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shuffleAnswers", + "columnName": "shuffleAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showCorrectAnswers", + "columnName": "showCorrectAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringPolicy", + "columnName": "scoringPolicy", + "affinity": "TEXT" + }, + { + "fieldPath": "accessCode", + "columnName": "accessCode", + "affinity": "TEXT" + }, + { + "fieldPath": "ipFilter", + "columnName": "ipFilter", + "affinity": "TEXT" + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT" + }, + { + "fieldPath": "hideResults", + "columnName": "hideResults", + "affinity": "TEXT" + }, + { + "fieldPath": "showCorrectAnswersAt", + "columnName": "showCorrectAnswersAt", + "affinity": "TEXT" + }, + { + "fieldPath": "hideCorrectAnswersAt", + "columnName": "hideCorrectAnswersAt", + "affinity": "TEXT" + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT" + }, + { + "fieldPath": "oneTimeResults", + "columnName": "oneTimeResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT" + }, + { + "fieldPath": "questionTypes", + "columnName": "questionTypes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasAccessCode", + "columnName": "hasAccessCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneQuestionAtATime", + "columnName": "oneQuestionAtATime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowser", + "columnName": "requireLockdownBrowser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowserForResults", + "columnName": "requireLockdownBrowserForResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowAnonymousSubmissions", + "columnName": "allowAnonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isOnlyVisibleToOverrides", + "columnName": "isOnlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockInfoEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `modulePrerequisiteNames` TEXT, `unlockAt` TEXT, `lockedModuleId` INTEGER, `assignmentId` INTEGER, `moduleId` INTEGER, `pageId` INTEGER, FOREIGN KEY(`moduleId`) REFERENCES `ModuleContentDetailsEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`pageId`) REFERENCES `PageEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "modulePrerequisiteNames", + "columnName": "modulePrerequisiteNames", + "affinity": "TEXT" + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT" + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER" + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER" + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER" + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "ModuleContentDetailsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "PageEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pageId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockedModuleEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `unlockAt` TEXT, `isRequireSequentialProgress` INTEGER NOT NULL, `lockInfoId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`lockInfoId`) REFERENCES `LockInfoEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT" + }, + { + "fieldPath": "isRequireSequentialProgress", + "columnName": "isRequireSequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockInfoId", + "columnName": "lockInfoId", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "LockInfoEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockInfoId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleNameEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `lockedModuleId` INTEGER NOT NULL, FOREIGN KEY(`lockedModuleId`) REFERENCES `LockedModuleEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "LockedModuleEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockedModuleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleCompletionRequirementEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT, `minScore` REAL NOT NULL, `maxScore` REAL NOT NULL, `completed` INTEGER, `moduleId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "minScore", + "columnName": "minScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxScore", + "columnName": "maxScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER" + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `fileName` TEXT, `courseId` INTEGER NOT NULL, `url` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT" + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "ConferenceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `conferenceKey` TEXT, `conferenceType` TEXT, `description` TEXT, `duration` INTEGER NOT NULL, `endedAt` INTEGER, `hasAdvancedSettings` INTEGER NOT NULL, `joinUrl` TEXT, `longRunning` INTEGER NOT NULL, `startedAt` INTEGER, `title` TEXT, `url` TEXT, `contextType` TEXT NOT NULL, `contextId` INTEGER NOT NULL, `record` INTEGER, `users` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conferenceKey", + "columnName": "conferenceKey", + "affinity": "TEXT" + }, + { + "fieldPath": "conferenceType", + "columnName": "conferenceType", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasAdvancedSettings", + "columnName": "hasAdvancedSettings", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinUrl", + "columnName": "joinUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "longRunning", + "columnName": "longRunning", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startedAt", + "columnName": "startedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "record", + "columnName": "record", + "affinity": "INTEGER" + }, + { + "fieldPath": "users", + "columnName": "users", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ConferenceRecordingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`recordingId` TEXT NOT NULL, `conferenceId` INTEGER NOT NULL, `createdAtMillis` INTEGER NOT NULL, `durationMinutes` INTEGER NOT NULL, `playbackUrl` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`recordingId`), FOREIGN KEY(`conferenceId`) REFERENCES `ConferenceEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "recordingId", + "columnName": "recordingId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conferenceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAtMillis", + "columnName": "createdAtMillis", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durationMinutes", + "columnName": "durationMinutes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playbackUrl", + "columnName": "playbackUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "recordingId" + ] + }, + "foreignKeys": [ + { + "table": "ConferenceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "conferenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFeaturesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `features` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contentType` TEXT, `filename` TEXT, `displayName` TEXT, `url` TEXT, `thumbnailUrl` TEXT, `previewUrl` TEXT, `createdAt` INTEGER, `size` INTEGER NOT NULL, `workerId` TEXT, `submissionCommentId` INTEGER, `submissionId` INTEGER, `attempt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionCommentId`) REFERENCES `SubmissionCommentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT" + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT" + }, + { + "fieldPath": "submissionCommentId", + "columnName": "submissionCommentId", + "affinity": "INTEGER" + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER" + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "SubmissionCommentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionCommentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MediaCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mediaId` TEXT NOT NULL, `submissionId` INTEGER NOT NULL, `attemptId` INTEGER NOT NULL, `displayName` TEXT, `url` TEXT, `mediaType` TEXT, `contentType` TEXT, PRIMARY KEY(`mediaId`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "mediaId", + "columnName": "mediaId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "mediaType", + "columnName": "mediaType", + "affinity": "TEXT" + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "mediaId" + ] + }, + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "AuthorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, `pronouns` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "SubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `authorId` INTEGER NOT NULL, `authorName` TEXT, `authorPronouns` TEXT, `comment` TEXT, `createdAt` INTEGER, `mediaCommentId` TEXT, `attemptId` INTEGER, `submissionId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorName", + "columnName": "authorName", + "affinity": "TEXT" + }, + { + "fieldPath": "authorPronouns", + "columnName": "authorPronouns", + "affinity": "TEXT" + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT" + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER" + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "DiscussionTopicEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unreadEntries` TEXT NOT NULL, `participantIds` TEXT NOT NULL, `viewIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadEntries", + "columnName": "unreadEntries", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantIds", + "columnName": "participantIds", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viewIds", + "columnName": "viewIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "CourseSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `tabs` TEXT NOT NULL, `additionalFilesStarted` INTEGER NOT NULL, `progressState` TEXT NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "additionalFilesStarted", + "columnName": "additionalFilesStarted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + } + }, + { + "tableName": "FileSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `additionalFile` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseSyncProgressEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "fileId", + "columnName": "fileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "additionalFile", + "columnName": "additionalFile", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "CourseSyncProgressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "StudioMediaProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ltiLaunchId` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "ltiLaunchId", + "columnName": "ltiLaunchId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "CustomGradeStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CheckpointEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `assignmentId` INTEGER, `name` TEXT, `tag` TEXT, `pointsPossible` REAL, `dueAt` TEXT, `onlyVisibleToOverrides` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, `moduleItemId` INTEGER, `courseId` INTEGER, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`moduleItemId`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT" + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL" + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT" + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT" + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT" + }, + { + "fieldPath": "moduleItemId", + "columnName": "moduleItemId", + "affinity": "INTEGER" + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleItemId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubAssignmentSubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `submissionId` INTEGER NOT NULL, `submissionAttempt` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `latePolicyStatus` TEXT, `customGradeStatusId` INTEGER, `subAssignmentTag` TEXT, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `userId` INTEGER NOT NULL, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, FOREIGN KEY(`submissionId`, `submissionAttempt`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionAttempt", + "columnName": "submissionAttempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT" + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latePolicyStatus", + "columnName": "latePolicyStatus", + "affinity": "TEXT" + }, + { + "fieldPath": "customGradeStatusId", + "columnName": "customGradeStatusId", + "affinity": "INTEGER" + }, + { + "fieldPath": "subAssignmentTag", + "columnName": "subAssignmentTag", + "affinity": "TEXT" + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT" + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "submissionAttempt" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3bcbb0a766401a1a19d753efa8b89959')" + ] + } +} \ No newline at end of file diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.studentdb.StudentDb/6.json b/libs/pandautils/schemas/com.instructure.pandautils.room.studentdb.StudentDb/6.json new file mode 100644 index 0000000000..905d716463 --- /dev/null +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.studentdb.StudentDb/6.json @@ -0,0 +1,356 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "5bcd6a554981819d2df188b0d0e2d48e", + "entities": [ + { + "tableName": "CreateSubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `submissionEntry` TEXT, `lastActivityDate` INTEGER, `assignmentName` TEXT, `assignmentId` INTEGER NOT NULL, `canvasContext` TEXT NOT NULL, `submissionType` TEXT NOT NULL, `errorFlag` INTEGER NOT NULL, `assignmentGroupCategoryId` INTEGER, `userId` INTEGER NOT NULL, `currentFile` INTEGER NOT NULL, `fileCount` INTEGER NOT NULL, `progress` REAL, `annotatableAttachmentId` INTEGER, `isDraft` INTEGER NOT NULL, `attempt` INTEGER NOT NULL, `mediaType` TEXT, `mediaSource` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionEntry", + "columnName": "submissionEntry", + "affinity": "TEXT" + }, + { + "fieldPath": "lastActivityDate", + "columnName": "lastActivityDate", + "affinity": "INTEGER" + }, + { + "fieldPath": "assignmentName", + "columnName": "assignmentName", + "affinity": "TEXT" + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canvasContext", + "columnName": "canvasContext", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionType", + "columnName": "submissionType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "errorFlag", + "columnName": "errorFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentGroupCategoryId", + "columnName": "assignmentGroupCategoryId", + "affinity": "INTEGER" + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentFile", + "columnName": "currentFile", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileCount", + "columnName": "fileCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "REAL" + }, + { + "fieldPath": "annotatableAttachmentId", + "columnName": "annotatableAttachmentId", + "affinity": "INTEGER" + }, + { + "fieldPath": "isDraft", + "columnName": "isDraft", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaType", + "columnName": "mediaType", + "affinity": "TEXT" + }, + { + "fieldPath": "mediaSource", + "columnName": "mediaSource", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "CreatePendingSubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountDomain` TEXT NOT NULL, `canvasContext` TEXT NOT NULL, `assignmentName` TEXT NOT NULL, `assignmentId` INTEGER NOT NULL, `lastActivityDate` INTEGER NOT NULL, `isGroupMessage` INTEGER NOT NULL, `message` TEXT, `mediaPath` TEXT, `currentFile` INTEGER NOT NULL, `fileCount` INTEGER NOT NULL, `progress` REAL, `errorFlag` INTEGER NOT NULL, `attemptId` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountDomain", + "columnName": "accountDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "canvasContext", + "columnName": "canvasContext", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assignmentName", + "columnName": "assignmentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivityDate", + "columnName": "lastActivityDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGroupMessage", + "columnName": "isGroupMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT" + }, + { + "fieldPath": "mediaPath", + "columnName": "mediaPath", + "affinity": "TEXT" + }, + { + "fieldPath": "currentFile", + "columnName": "currentFile", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileCount", + "columnName": "fileCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "REAL" + }, + { + "fieldPath": "errorFlag", + "columnName": "errorFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "CreateFileSubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dbSubmissionId` INTEGER NOT NULL, `attachmentId` INTEGER, `name` TEXT, `size` INTEGER, `contentType` TEXT, `fullPath` TEXT, `error` TEXT, `errorFlag` INTEGER NOT NULL, FOREIGN KEY(`dbSubmissionId`) REFERENCES `CreateSubmissionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dbSubmissionId", + "columnName": "dbSubmissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachmentId", + "columnName": "attachmentId", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER" + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT" + }, + { + "fieldPath": "fullPath", + "columnName": "fullPath", + "affinity": "TEXT" + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT" + }, + { + "fieldPath": "errorFlag", + "columnName": "errorFlag", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "CreateSubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "dbSubmissionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CreateSubmissionCommentFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `pendingCommentId` INTEGER NOT NULL, `attachmentId` INTEGER, `name` TEXT NOT NULL, `size` INTEGER NOT NULL, `contentType` TEXT NOT NULL, `fullPath` TEXT NOT NULL, FOREIGN KEY(`pendingCommentId`) REFERENCES `CreatePendingSubmissionCommentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pendingCommentId", + "columnName": "pendingCommentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachmentId", + "columnName": "attachmentId", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullPath", + "columnName": "fullPath", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "CreatePendingSubmissionCommentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pendingCommentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5bcd6a554981819d2df188b0d0e2d48e')" + ] + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/assignments/details/DiscussionCheckpointLayoutTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/assignments/details/DiscussionCheckpointLayoutTest.kt new file mode 100644 index 0000000000..e8b68b8447 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/assignments/details/DiscussionCheckpointLayoutTest.kt @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.pandautils.compose.features.assignments.details + +import android.graphics.Color +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import com.instructure.pandares.R +import com.instructure.pandautils.features.assignments.details.DiscussionCheckpointViewState +import com.instructure.pandautils.features.assignments.details.composables.DiscussionCheckpointLayout +import com.instructure.pandautils.features.grades.SubmissionStateLabel +import org.junit.Rule +import org.junit.Test + +class DiscussionCheckpointLayoutTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun checkpointsAreDisplayedCorrectly() { + val checkpoints = listOf( + DiscussionCheckpointViewState( + name = "Reply to topic", + stateLabel = SubmissionStateLabel.Companion.Graded, + grade = "5 / 5 pts", + courseColor = Color.RED + ), + DiscussionCheckpointViewState( + name = "Additional replies (3)", + stateLabel = SubmissionStateLabel.Companion.Late, + grade = "3 / 5 pts", + courseColor = Color.RED + ) + ) + + composeTestRule.setContent { + DiscussionCheckpointLayout(checkpoints = checkpoints) + } + + composeTestRule.onNodeWithTag("checkpointItem-0") + .assertIsDisplayed() + + composeTestRule.onNode(hasTestTag("checkpointName") and hasText("Reply to topic"), useUnmergedTree = true) + .assertIsDisplayed() + + composeTestRule.onNode(hasTestTag("checkpointStatus") and hasText("Graded"), useUnmergedTree = true) + .assertIsDisplayed() + + composeTestRule.onNode(hasTestTag("checkpointGrade") and hasText("5 / 5 pts"), useUnmergedTree = true) + .assertIsDisplayed() + + composeTestRule.onNodeWithTag("checkpointItem-1") + .assertIsDisplayed() + + composeTestRule.onNode(hasTestTag("checkpointName") and hasText("Additional replies (3)"), useUnmergedTree = true) + .assertIsDisplayed() + + composeTestRule.onNode(hasTestTag("checkpointStatus") and hasText("Late"), useUnmergedTree = true) + .assertIsDisplayed() + + composeTestRule.onNode(hasTestTag("checkpointGrade") and hasText("3 / 5 pts"), useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun emptyCheckpointsListRendersNothing() { + composeTestRule.setContent { + DiscussionCheckpointLayout(checkpoints = emptyList()) + } + + composeTestRule.onNodeWithTag("checkpointItem-0") + .assertDoesNotExist() + } + + @Test + fun singleCheckpointIsDisplayedWithoutDivider() { + val checkpoints = listOf( + DiscussionCheckpointViewState( + name = "Reply to topic", + stateLabel = SubmissionStateLabel.Companion.Graded, + grade = "5 / 5 pts", + courseColor = Color.BLUE + ) + ) + + composeTestRule.setContent { + DiscussionCheckpointLayout(checkpoints = checkpoints) + } + + composeTestRule.onNodeWithTag("checkpointItem-0") + .assertIsDisplayed() + + composeTestRule.onNode(hasTestTag("checkpointName") and hasText("Reply to topic"), useUnmergedTree = true) + .assertIsDisplayed() + + composeTestRule.onNode(hasTestTag("checkpointStatus") and hasText("Graded"), useUnmergedTree = true) + .assertIsDisplayed() + + composeTestRule.onNode(hasTestTag("checkpointGrade") and hasText("5 / 5 pts"), useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun checkpointWithCustomStatusIsDisplayedCorrectly() { + val checkpoints = listOf( + DiscussionCheckpointViewState( + name = "Reply to topic", + stateLabel = SubmissionStateLabel.Custom( + iconRes = R.drawable.ic_complete, + colorRes = R.color.textWarning, + label = "In Review" + ), + grade = "10 / 10 pts", + courseColor = Color.GREEN + ) + ) + + composeTestRule.setContent { + DiscussionCheckpointLayout(checkpoints = checkpoints) + } + + composeTestRule.onNodeWithTag("checkpointItem-0") + .assertIsDisplayed() + + composeTestRule.onNode(hasTestTag("checkpointName") and hasText("Reply to topic"), useUnmergedTree = true) + .assertIsDisplayed() + + composeTestRule.onNode(hasTestTag("checkpointStatus") and hasText("In Review"), useUnmergedTree = true) + .assertIsDisplayed() + + composeTestRule.onNode(hasTestTag("checkpointGrade") and hasText("10 / 10 pts"), useUnmergedTree = true) + .assertIsDisplayed() + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/assignments/list/AssignmentListScreenTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/assignments/list/AssignmentListScreenTest.kt index 1e9801c5bf..34fe628023 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/assignments/list/AssignmentListScreenTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/assignments/list/AssignmentListScreenTest.kt @@ -370,7 +370,7 @@ class AssignmentListScreenTest { composeTestRule.onNodeWithText("Checkpoint 1") .assertIsDisplayed() - composeTestRule.onNode(hasTestTag("checkpointDueDate") and hasText("Due date 1"), true) + composeTestRule.onNode(hasTestTag("checkpointDueDate_Checkpoint 1") and hasText("Due date 1"), true) .assertIsDisplayed() composeTestRule.onNode(hasTestTag("checkpointSubmissionStateLabel") and hasText("Graded"), true) .assertIsDisplayed() @@ -379,7 +379,7 @@ class AssignmentListScreenTest { composeTestRule.onNodeWithText("Checkpoint 2") .assertIsDisplayed() - composeTestRule.onNode(hasTestTag("checkpointDueDate") and hasText("Due date 2"), true) + composeTestRule.onNode(hasTestTag("checkpointDueDate_Checkpoint 2") and hasText("Due date 2"), true) .assertIsDisplayed() composeTestRule.onNode(hasTestTag("checkpointSubmissionStateLabel") and hasText("Missing"), true) .assertIsDisplayed() diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesAssignmentItemTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesAssignmentItemTest.kt index a5a6efcb9a..7f4266ea8e 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesAssignmentItemTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesAssignmentItemTest.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.composeTest.hasDrawable +import com.instructure.composetest.hasDrawable import com.instructure.espresso.assertTextColor import com.instructure.pandares.R import com.instructure.pandautils.features.grades.AssignmentItem diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesScreenTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesScreenTest.kt index 0aedb86e14..a389be9073 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesScreenTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/grades/GradesScreenTest.kt @@ -28,7 +28,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.composeTest.hasDrawable +import com.instructure.composetest.hasDrawable import com.instructure.pandares.R import com.instructure.pandautils.compose.composables.DiscussionCheckpointUiState import com.instructure.pandautils.features.grades.AssignmentGroupUiState @@ -249,7 +249,7 @@ class GradesScreenTest { composeTestRule.onNodeWithText("Checkpoint 1") .assertIsDisplayed() - composeTestRule.onNode(hasTestTag("checkpointDueDate") and hasText("Due date 1"), true) + composeTestRule.onNode(hasTestTag("checkpointDueDate_Checkpoint 1") and hasText("Due date 1"), true) .assertIsDisplayed() composeTestRule.onNode(hasTestTag("checkpointSubmissionStateLabel") and hasText("Graded"), true) .assertIsDisplayed() @@ -258,7 +258,7 @@ class GradesScreenTest { composeTestRule.onNodeWithText("Checkpoint 2") .assertIsDisplayed() - composeTestRule.onNode(hasTestTag("checkpointDueDate") and hasText("Due date 2"), true) + composeTestRule.onNode(hasTestTag("checkpointDueDate_Checkpoint 2") and hasText("Due date 2"), true) .assertIsDisplayed() composeTestRule.onNode(hasTestTag("checkpointSubmissionStateLabel") and hasText("Missing"), true) .assertIsDisplayed() diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/settings/SettingsScreenTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/settings/SettingsScreenTest.kt index 4aed3d92a7..56ee96a137 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/settings/SettingsScreenTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/settings/SettingsScreenTest.kt @@ -72,7 +72,20 @@ class SettingsScreenTest { } items.forEach { (title, items) -> - composeTestRule.onNodeWithText(context.getString(title)).assertExists() + retry(catchBlock = { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + val y = device.displayHeight / 2 + val x = device.displayWidth / 2 + device.swipe( + x, + y, + x, + 0, + 10 + ) + }) { + composeTestRule.onNodeWithText(context.getString(title)).assertExists() + } items.forEach { item -> retry(catchBlock = { val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/smartsearch/SmartSearchPreferencesScreenTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/smartsearch/SmartSearchPreferencesScreenTest.kt index 0ddeab05da..89c6b2abcb 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/smartsearch/SmartSearchPreferencesScreenTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/smartsearch/SmartSearchPreferencesScreenTest.kt @@ -50,7 +50,8 @@ class SmartSearchPreferencesScreenTest { color = Color.Magenta, filters = listOf(SmartSearchFilter.ASSIGNMENTS, SmartSearchFilter.ANNOUNCEMENTS), sortType = SmartSearchSortType.RELEVANCE, - navigationClick = {_, _ ->} + onDone = { _, _ -> }, + onCancel = {} ) } @@ -92,7 +93,8 @@ class SmartSearchPreferencesScreenTest { color = Color.Magenta, filters = listOf(SmartSearchFilter.ASSIGNMENTS, SmartSearchFilter.ANNOUNCEMENTS), sortType = SmartSearchSortType.RELEVANCE, - navigationClick = {_, _ ->} + onDone = { _, _ -> }, + onCancel = {} ) } @@ -138,7 +140,8 @@ class SmartSearchPreferencesScreenTest { color = Color.Magenta, filters = emptyList(), sortType = SmartSearchSortType.RELEVANCE, - navigationClick = {_, _ ->} + onDone = { _, _ -> }, + onCancel = {} ) } @@ -164,7 +167,8 @@ class SmartSearchPreferencesScreenTest { color = Color.Magenta, filters = emptyList(), sortType = SmartSearchSortType.RELEVANCE, - navigationClick = {_, _ ->} + onDone = { _, _ -> }, + onCancel = {} ) } @@ -187,7 +191,8 @@ class SmartSearchPreferencesScreenTest { color = Color.Magenta, filters = emptyList(), sortType = SmartSearchSortType.TYPE, - navigationClick = {_, _ ->} + onDone = { _, _ -> }, + onCancel = {} ) } @@ -213,7 +218,8 @@ class SmartSearchPreferencesScreenTest { color = Color.Magenta, filters = emptyList(), sortType = SmartSearchSortType.RELEVANCE, - navigationClick = {_, _ ->} + onDone = { _, _ -> }, + onCancel = {} ) } diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/smartsearch/SmartSearchScreenTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/smartsearch/SmartSearchScreenTest.kt index f7a375f8a0..fb4110765e 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/smartsearch/SmartSearchScreenTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/smartsearch/SmartSearchScreenTest.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.SmartSearchContentType -import com.instructure.composeTest.hasSiblingWithText +import com.instructure.composetest.hasSiblingWithText import com.instructure.pandautils.features.smartsearch.SmartSearchResultUiState import com.instructure.pandautils.features.smartsearch.SmartSearchScreen import com.instructure.pandautils.features.smartsearch.SmartSearchSortType diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt new file mode 100644 index 0000000000..abd9fe78a9 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CheckpointDaoTest.kt @@ -0,0 +1,453 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.Checkpoint +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.AssignmentEntity +import com.instructure.pandautils.room.offline.entities.AssignmentGroupEntity +import com.instructure.pandautils.room.offline.entities.CheckpointEntity +import com.instructure.pandautils.room.offline.entities.CourseEntity +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class CheckpointDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var checkpointDao: CheckpointDao + private lateinit var assignmentDao: AssignmentDao + private lateinit var assignmentGroupDao: AssignmentGroupDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + checkpointDao = db.checkpointDao() + assignmentDao = db.assignmentDao() + assignmentGroupDao = db.assignmentGroupDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertAndFind() = runTest { + setupAssignment(1L, 1L) + + val checkpoint = CheckpointEntity( + assignmentId = 1L, + name = "Checkpoint 1", + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null + ) + + checkpointDao.insert(checkpoint) + + val result = checkpointDao.findByAssignmentId(1L) + assertEquals(1, result.size) + assertEquals("Checkpoint 1", result[0].name) + assertEquals("reply_to_topic", result[0].tag) + assertEquals(10.0, result[0].pointsPossible) + } + + @Test + fun testInsertMultipleCheckpoints() = runTest { + setupAssignment(1L, 1L) + + val checkpoint1 = CheckpointEntity( + assignmentId = 1L, + name = "Reply to Topic", + tag = "reply_to_topic", + pointsPossible = 5.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null + ) + + val checkpoint2 = CheckpointEntity( + assignmentId = 1L, + name = "Required Replies", + tag = "reply_to_entry", + pointsPossible = 5.0, + dueAt = "2025-10-20T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null + ) + + checkpointDao.insertAll(listOf(checkpoint1, checkpoint2)) + + val result = checkpointDao.findByAssignmentId(1L) + assertEquals(2, result.size) + assertTrue(result.any { it.tag == "reply_to_topic" }) + assertTrue(result.any { it.tag == "reply_to_entry" }) + } + + @Test + fun testDeleteByAssignmentId() = runTest { + setupAssignment(1L, 1L) + + val checkpoint = CheckpointEntity( + assignmentId = 1L, + name = "Checkpoint 1", + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null + ) + + checkpointDao.insert(checkpoint) + checkpointDao.deleteByAssignmentId(1L) + + val result = checkpointDao.findByAssignmentId(1L) + assertTrue(result.isEmpty()) + } + + @Test + fun testCascadeDelete() = runTest { + setupAssignment(1L, 1L) + + val checkpoint = CheckpointEntity( + assignmentId = 1L, + name = "Checkpoint 1", + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null + ) + + checkpointDao.insert(checkpoint) + + val assignmentEntity = assignmentDao.findById(1L)!! + assignmentDao.delete(assignmentEntity) + + val result = checkpointDao.findByAssignmentId(1L) + assertTrue(result.isEmpty()) + } + + @Test + fun testToApiModel() { + val checkpointEntity = CheckpointEntity( + assignmentId = 1L, + name = "Reply to Topic", + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = true, + lockAt = "2025-10-22T23:59:59Z", + unlockAt = "2025-10-10T00:00:00Z" + ) + + val checkpoint = checkpointEntity.toApiModel() + + assertEquals("Reply to Topic", checkpoint.name) + assertEquals("reply_to_topic", checkpoint.tag) + assertEquals(10.0, checkpoint.pointsPossible) + assertEquals("2025-10-15T23:59:59Z", checkpoint.dueAt) + assertEquals(true, checkpoint.onlyVisibleToOverrides) + assertEquals("2025-10-22T23:59:59Z", checkpoint.lockAt) + assertEquals("2025-10-10T00:00:00Z", checkpoint.unlockAt) + } + + @Test + fun testConstructorFromApiModel() { + val checkpoint = Checkpoint( + name = "Reply to Topic", + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + overrides = null, + onlyVisibleToOverrides = true, + lockAt = "2025-10-22T23:59:59Z", + unlockAt = "2025-10-10T00:00:00Z" + ) + + val entity = CheckpointEntity(checkpoint, 1L) + + assertEquals(1L, entity.assignmentId) + assertEquals("Reply to Topic", entity.name) + assertEquals("reply_to_topic", entity.tag) + assertEquals(10.0, entity.pointsPossible) + assertEquals("2025-10-15T23:59:59Z", entity.dueAt) + assertEquals(true, entity.onlyVisibleToOverrides) + assertEquals("2025-10-22T23:59:59Z", entity.lockAt) + assertEquals("2025-10-10T00:00:00Z", entity.unlockAt) + } + + @Test + fun testFindByCourseIdWithModuleItem() = runTest { + setupCourseAndModule(1L, 100L) + + val checkpoint1 = CheckpointEntity( + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 5.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + + val checkpoint2 = CheckpointEntity( + assignmentId = null, + name = null, + tag = "reply_to_entry", + pointsPossible = 5.0, + dueAt = "2025-10-20T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + + checkpointDao.insertAll(listOf(checkpoint1, checkpoint2)) + + val result = checkpointDao.findByCourseIdWithModuleItem(1L) + + assertEquals(2, result.size) + assertTrue(result.all { it.courseId == 1L }) + assertTrue(result.all { it.moduleItemId == 100L }) + } + + @Test + fun testFindByCourseIdWithModuleItemFiltersNullModuleItemIds() = runTest { + setupAssignment(1L, 1L) + + val checkpointWithAssignment = CheckpointEntity( + assignmentId = 1L, + name = null, + tag = "reply_to_topic", + pointsPossible = 5.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = null, + courseId = 1L + ) + + checkpointDao.insert(checkpointWithAssignment) + + val result = checkpointDao.findByCourseIdWithModuleItem(1L) + + assertTrue(result.isEmpty()) + } + + @Test + fun testFindByCourseIdWithModuleItemFiltersOtherCourses() = runTest { + setupCourseAndModule(1L, 100L) + setupCourseAndModule(2L, 200L) + + val checkpoint1 = CheckpointEntity( + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 5.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + + val checkpoint2 = CheckpointEntity( + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 200L, + courseId = 2L + ) + + checkpointDao.insertAll(listOf(checkpoint1, checkpoint2)) + + val result = checkpointDao.findByCourseIdWithModuleItem(1L) + + assertEquals(1, result.size) + assertEquals(1L, result[0].courseId) + assertEquals(100L, result[0].moduleItemId) + } + + @Test + fun testToModuleItemCheckpoint() { + val checkpointEntity = CheckpointEntity( + id = 1, + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + + val moduleItemCheckpoint = checkpointEntity.toModuleItemCheckpoint() + + assertEquals("reply_to_topic", moduleItemCheckpoint.tag) + assertEquals(10.0, moduleItemCheckpoint.pointsPossible, 0.01) + assertTrue(moduleItemCheckpoint.dueAt != null) + } + + @Test + fun testToModuleItemCheckpointWithNullDueAt() { + val checkpointEntity = CheckpointEntity( + id = 1, + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = null, + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + + val moduleItemCheckpoint = checkpointEntity.toModuleItemCheckpoint() + + assertEquals("reply_to_topic", moduleItemCheckpoint.tag) + assertEquals(10.0, moduleItemCheckpoint.pointsPossible, 0.01) + assertEquals(null, moduleItemCheckpoint.dueAt) + } + + @Test + fun testInsertCheckpointWithModuleItemId() = runTest { + setupCourseAndModule(1L, 100L) + + val checkpoint = CheckpointEntity( + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + + checkpointDao.insert(checkpoint) + + val result = checkpointDao.findByCourseIdWithModuleItem(1L) + + assertEquals(1, result.size) + assertEquals(100L, result[0].moduleItemId) + assertEquals(null, result[0].assignmentId) + } + + @Test + fun testCascadeDeleteWithModuleItem() = runTest { + setupCourseAndModule(1L, 100L) + + val checkpoint = CheckpointEntity( + assignmentId = null, + name = null, + tag = "reply_to_topic", + pointsPossible = 10.0, + dueAt = "2025-10-15T23:59:59Z", + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = 100L, + courseId = 1L + ) + + checkpointDao.insert(checkpoint) + + val moduleItemDao = db.moduleItemDao() + val moduleItem = moduleItemDao.findById(100L)!! + moduleItemDao.delete(moduleItem) + + val result = checkpointDao.findByCourseIdWithModuleItem(1L) + + assertTrue(result.isEmpty()) + } + + private suspend fun setupAssignment(assignmentId: Long, courseId: Long) { + val courseEntity = CourseEntity(Course(id = courseId)) + courseDao.insert(courseEntity) + + val assignmentGroupEntity = AssignmentGroupEntity(AssignmentGroup(id = 1L), courseId) + assignmentGroupDao.insert(assignmentGroupEntity) + + val assignmentEntity = AssignmentEntity( + Assignment(id = assignmentId, name = "Test Assignment", assignmentGroupId = 1L, courseId = courseId), + null, null, null, null + ) + assignmentDao.insert(assignmentEntity) + } + + private suspend fun setupCourseAndModule(courseId: Long, moduleItemId: Long) { + val courseEntity = CourseEntity(Course(id = courseId)) + courseDao.insert(courseEntity) + + val moduleObjectDao = db.moduleObjectDao() + val moduleObjectEntity = com.instructure.pandautils.room.offline.entities.ModuleObjectEntity( + com.instructure.canvasapi2.models.ModuleObject(id = courseId, name = "Test Module"), + courseId + ) + moduleObjectDao.insert(moduleObjectEntity) + + val moduleItemDao = db.moduleItemDao() + val moduleItemEntity = com.instructure.pandautils.room.offline.entities.ModuleItemEntity( + com.instructure.canvasapi2.models.ModuleItem(id = moduleItemId), + moduleId = courseId + ) + moduleItemDao.insert(moduleItemEntity) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/GradingPeriodDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/GradingPeriodDaoTest.kt index 3b786f8448..1badd32d6e 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/GradingPeriodDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/GradingPeriodDaoTest.kt @@ -61,15 +61,13 @@ class GradingPeriodDaoTest { Assert.assertEquals(gradingPeriodEntity, result) } - @Test - fun testFindEntityByIdReturnsNullIfNotFound() = runTest { + @Test(expected = IllegalStateException::class) + fun testFindEntityByIdThrowsExceptionIfNotFound() = runTest { val gradingPeriodEntity = GradingPeriodEntity(GradingPeriod(id = 1, "Grading period 1")) val gradingPeriodEntity2 = GradingPeriodEntity(GradingPeriod(id = 2, "Grading period 2")) gradingPeriodDao.insert(gradingPeriodEntity) gradingPeriodDao.insert(gradingPeriodEntity2) val result = gradingPeriodDao.findById(3) - - Assert.assertNull(result) } } \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/PlannerItemDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/PlannerItemDaoTest.kt new file mode 100644 index 0000000000..1a7d41f03c --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/PlannerItemDaoTest.kt @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Plannable +import com.instructure.canvasapi2.models.PlannableType +import com.instructure.canvasapi2.models.PlannerItem +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.PlannerItemEntity +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Date + +@RunWith(AndroidJUnit4::class) +class PlannerItemDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var plannerItemDao: PlannerItemDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + plannerItemDao = db.plannerItemDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindById() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + val plannerItems = createPlannerItems(listOf(1L, 2L), 1L) + val expectedPlannerItem = plannerItems[1] + + plannerItems.forEach { + plannerItemDao.insert(it) + } + + val result = plannerItemDao.findById(2L) + + assertEquals(expectedPlannerItem.plannableTitle, result?.plannableTitle) + } + + @Test + fun testFindByCourseId() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + courseDao.insert(CourseEntity(Course(2L))) + val plannerItems = listOf( + createPlannerItem(1L, 2L, "Item 1"), + createPlannerItem(2L, 1L, "Item 2"), + createPlannerItem(3L, 2L, "Item 3") + ) + val expectedItems = plannerItems.filter { it.courseId == 2L } + + plannerItems.forEach { plannerItemDao.insert(it) } + + val result = plannerItemDao.findByCourseId(2L) + assertEquals(expectedItems.map { it.plannableTitle }, result.map { it.plannableTitle }) + } + + @Test + fun testFindByCourseIds() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + courseDao.insert(CourseEntity(Course(2L))) + courseDao.insert(CourseEntity(Course(3L))) + val plannerItems = listOf( + createPlannerItem(1L, 1L, "Item 1"), + createPlannerItem(2L, 2L, "Item 2"), + createPlannerItem(3L, 3L, "Item 3"), + createPlannerItem(4L, 1L, "Item 4") + ) + val expectedItems = plannerItems.filter { it.courseId in listOf(1L, 2L) } + + plannerItems.forEach { plannerItemDao.insert(it) } + + val result = plannerItemDao.findByCourseIds(listOf(1L, 2L)) + assertEquals(expectedItems.map { it.plannableTitle }.sortedBy { it }, result.map { it.plannableTitle }.sortedBy { it }) + } + + @Test + fun testInsertReplace() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + val plannerItem1 = createPlannerItem(1L, 1L, "Item 1") + val plannerItem2 = createPlannerItem(1L, 1L, "Item 2") + + plannerItemDao.insert(plannerItem1) + plannerItemDao.insert(plannerItem2) + + val result = plannerItemDao.findById(1L) + + assertEquals(plannerItem2.plannableTitle, result?.plannableTitle) + } + + @Test(expected = SQLiteConstraintException::class) + fun testForeignKeyConstraint() = runTest { + plannerItemDao.insert(createPlannerItem(1L, 1L, "Item 1")) + } + + @Test + fun testInsertAll() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + + val plannerItems = createPlannerItems(listOf(1L, 2L, 3L), 1L) + plannerItemDao.insertAll(plannerItems) + + val result = plannerItemDao.findByCourseId(1L) + assertEquals(plannerItems.size, result.size) + } + + @Test + fun testDeleteAllByCourseId() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + courseDao.insert(CourseEntity(Course(2L))) + + val plannerItems = listOf( + createPlannerItem(1L, 1L, "Item 1"), + createPlannerItem(2L, 1L, "Item 2"), + createPlannerItem(3L, 2L, "Item 3") + ) + plannerItems.forEach { plannerItemDao.insert(it) } + + val resultBefore = plannerItemDao.findByCourseId(1L) + assertEquals(2, resultBefore.size) + + plannerItemDao.deleteAllByCourseId(1L) + + val resultAfter = plannerItemDao.findByCourseId(1L) + Assert.assertTrue(resultAfter.isEmpty()) + + val course2Items = plannerItemDao.findByCourseId(2L) + assertEquals(1, course2Items.size) + } + + @Test + fun testUpdate() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + val plannerItem = createPlannerItem(1L, 1L, "Item 1") + plannerItemDao.insert(plannerItem) + + val updatedItem = plannerItem.copy(plannableTitle = "Updated Item") + plannerItemDao.update(updatedItem) + + val result = plannerItemDao.findById(1L) + assertEquals("Updated Item", result?.plannableTitle) + } + + @Test + fun testDelete() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + val plannerItem = createPlannerItem(1L, 1L, "Item 1") + plannerItemDao.insert(plannerItem) + + val resultBefore = plannerItemDao.findById(1L) + assertEquals(plannerItem.plannableTitle, resultBefore?.plannableTitle) + + plannerItemDao.delete(plannerItem) + + val resultAfter = plannerItemDao.findById(1L) + assertEquals(null, resultAfter) + } + + private fun createPlannerItem(id: Long, courseId: Long, title: String): PlannerItemEntity { + val plannable = Plannable( + id = id, + title = title, + courseId = courseId, + groupId = null, + userId = null, + pointsPossible = 10.0, + dueAt = Date(), + assignmentId = id, + todoDate = null, + startAt = null, + endAt = null, + details = "Details for $title", + allDay = false + ) + val plannerItem = PlannerItem( + courseId = courseId, + groupId = null, + userId = null, + contextType = "course", + contextName = "Course $courseId", + plannableType = PlannableType.ASSIGNMENT, + plannable = plannable, + plannableDate = Date(), + htmlUrl = "https://example.com/item/$id", + submissionState = null, + newActivity = false + ) + return PlannerItemEntity(plannerItem, courseId) + } + + private fun createPlannerItems(ids: List, courseId: Long): List { + return ids.map { createPlannerItem(it, courseId, "Item $it") } + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDaoTest.kt new file mode 100644 index 0000000000..7596709d0b --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubAssignmentSubmissionDaoTest.kt @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.SubAssignmentSubmission +import com.instructure.canvasapi2.models.Submission +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.AssignmentEntity +import com.instructure.pandautils.room.offline.entities.AssignmentGroupEntity +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.SubAssignmentSubmissionEntity +import com.instructure.pandautils.room.offline.entities.SubmissionEntity +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SubAssignmentSubmissionDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var subAssignmentSubmissionDao: SubAssignmentSubmissionDao + private lateinit var submissionDao: SubmissionDao + private lateinit var assignmentDao: AssignmentDao + private lateinit var assignmentGroupDao: AssignmentGroupDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + subAssignmentSubmissionDao = db.subAssignmentSubmissionDao() + submissionDao = db.submissionDao() + assignmentDao = db.assignmentDao() + assignmentGroupDao = db.assignmentGroupDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertAndFind() = runTest { + setupSubmission(1L, 1L, 1L) + + val subAssignmentSubmission = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "A", + score = 10.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + subAssignmentSubmissionDao.insert(subAssignmentSubmission) + + val result = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 1L) + assertEquals(1, result.size) + assertEquals("A", result[0].grade) + assertEquals(10.0, result[0].score) + assertEquals("reply_to_topic", result[0].subAssignmentTag) + } + + @Test + fun testInsertMultipleSubAssignmentSubmissions() = runTest { + setupSubmission(1L, 1L, 1L) + + val subAssignment1 = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "A", + score = 5.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 5.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + val subAssignment2 = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "B", + score = 4.0, + late = true, + excused = false, + missing = false, + latePolicyStatus = "late", + customGradeStatusId = null, + subAssignmentTag = "reply_to_entry", + enteredScore = 5.0, + enteredGrade = "B", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + subAssignmentSubmissionDao.insertAll(listOf(subAssignment1, subAssignment2)) + + val result = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 1L) + assertEquals(2, result.size) + assertTrue(result.any { it.subAssignmentTag == "reply_to_topic" && it.score == 5.0 }) + assertTrue(result.any { it.subAssignmentTag == "reply_to_entry" && it.late }) + } + + @Test + fun testDeleteBySubmissionIdAndAttempt() = runTest { + setupSubmission(1L, 1L, 1L) + + val subAssignmentSubmission = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "A", + score = 10.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + subAssignmentSubmissionDao.insert(subAssignmentSubmission) + subAssignmentSubmissionDao.deleteBySubmissionIdAndAttempt(1L, 1L) + + val result = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 1L) + assertTrue(result.isEmpty()) + } + + @Test + fun testCascadeDelete() = runTest { + setupSubmission(1L, 1L, 1L) + + val subAssignmentSubmission = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "A", + score = 10.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + subAssignmentSubmissionDao.insert(subAssignmentSubmission) + + val submissions = submissionDao.findById(1L) + val submissionEntity = submissions.first { it.id == 1L && it.attempt == 1L } + submissionDao.delete(submissionEntity) + + val result = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 1L) + assertTrue(result.isEmpty()) + } + + @Test + fun testMultipleAttempts() = runTest { + setupSubmission(1L, 1L, 1L) + + val submission2 = SubmissionEntity( + Submission(id = 1L, assignmentId = 1L, attempt = 2L), + null, + null + ) + submissionDao.insert(submission2) + + val subAssignment1 = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "B", + score = 8.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 8.0, + enteredGrade = "B", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + val subAssignment2 = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 2L, + grade = "A", + score = 10.0, + late = false, + excused = false, + missing = false, + latePolicyStatus = null, + customGradeStatusId = null, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + subAssignmentSubmissionDao.insertAll(listOf(subAssignment1, subAssignment2)) + + val attempt1Results = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 1L) + val attempt2Results = subAssignmentSubmissionDao.findBySubmissionIdAndAttempt(1L, 2L) + + assertEquals(1, attempt1Results.size) + assertEquals(8.0, attempt1Results[0].score) + assertEquals(1, attempt2Results.size) + assertEquals(10.0, attempt2Results[0].score) + } + + @Test + fun testToApiModel() { + val entity = SubAssignmentSubmissionEntity( + submissionId = 1L, + submissionAttempt = 1L, + grade = "A", + score = 10.0, + late = true, + excused = false, + missing = false, + latePolicyStatus = "late", + customGradeStatusId = 123L, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + val apiModel = entity.toApiModel() + + assertEquals("A", apiModel.grade) + assertEquals(10.0, apiModel.score) + assertEquals(true, apiModel.late) + assertEquals(false, apiModel.excused) + assertEquals(false, apiModel.missing) + assertEquals("late", apiModel.latePolicyStatus) + assertEquals(123L, apiModel.customGradeStatusId) + assertEquals("reply_to_topic", apiModel.subAssignmentTag) + assertEquals(10.0, apiModel.enteredScore) + assertEquals("A", apiModel.enteredGrade) + assertEquals(1L, apiModel.userId) + assertEquals(true, apiModel.isGradeMatchesCurrentSubmission) + } + + @Test + fun testConstructorFromApiModel() { + val apiModel = SubAssignmentSubmission( + grade = "A", + score = 10.0, + late = true, + excused = false, + missing = false, + latePolicyStatus = "late", + customGradeStatusId = 123L, + subAssignmentTag = "reply_to_topic", + enteredScore = 10.0, + enteredGrade = "A", + userId = 1L, + isGradeMatchesCurrentSubmission = true + ) + + val entity = SubAssignmentSubmissionEntity(apiModel, 1L, 2L) + + assertEquals(1L, entity.submissionId) + assertEquals(2L, entity.submissionAttempt) + assertEquals("A", entity.grade) + assertEquals(10.0, entity.score) + assertEquals(true, entity.late) + assertEquals("reply_to_topic", entity.subAssignmentTag) + } + + private suspend fun setupSubmission(submissionId: Long, assignmentId: Long, courseId: Long) { + val courseEntity = CourseEntity(Course(id = courseId)) + courseDao.insert(courseEntity) + + val assignmentGroupEntity = AssignmentGroupEntity(AssignmentGroup(id = 1L), courseId) + assignmentGroupDao.insert(assignmentGroupEntity) + + val assignmentEntity = AssignmentEntity( + Assignment(id = assignmentId, name = "Test Assignment", assignmentGroupId = 1L, courseId = courseId), + null, null, null, null + ) + assignmentDao.insert(assignmentEntity) + + val submissionEntity = SubmissionEntity( + Submission(id = submissionId, assignmentId = assignmentId, attempt = 1L), + null, + null + ) + submissionDao.insert(submissionEntity) + } +} diff --git a/libs/pandautils/src/main/AndroidManifest.xml b/libs/pandautils/src/main/AndroidManifest.xml index 03eee83108..c267f2fc1b 100644 --- a/libs/pandautils/src/main/AndroidManifest.xml +++ b/libs/pandautils/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ + Unit, modifier: Modifier = Modifier, + subtitle: String? = null, testTag: String = "checkboxText" ) { - var checked by remember { mutableStateOf(selected) } - Row ( - modifier = modifier.clickable { checked = !checked }.padding(vertical = 8.dp, horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .clickable { onCheckedChanged(!selected) } ) { Checkbox( - checked = checked, + checked = selected, onCheckedChange = { - checked = it onCheckedChanged(it) }, colors = CheckboxDefaults.colors( @@ -62,12 +59,22 @@ fun CheckboxText( ), modifier = Modifier.testTag(testTag) ) - Text( - text = text, - fontSize = 16.sp, - lineHeight = 21.sp, - color = colorResource(R.color.textDarkest) - ) + Column { + subtitle?.let { + Text( + text = it, + fontSize = 14.sp, + lineHeight = 18.sp, + color = colorResource(R.color.textDark) + ) + } + Text( + text = text, + fontSize = 16.sp, + lineHeight = 21.sp, + color = colorResource(R.color.textDarkest) + ) + } } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/ChekcpointItem.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/ChekcpointItem.kt index 5933ec2d16..00c6ced98a 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/ChekcpointItem.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/ChekcpointItem.kt @@ -61,14 +61,15 @@ fun CheckpointItem( Text( text = discussionCheckpointUiState.name, color = colorResource(id = R.color.textDarkest), - fontSize = 16.sp + fontSize = 16.sp, + modifier = Modifier.testTag("checkpointName") ) FlowRow { Text( text = discussionCheckpointUiState.dueDate, color = colorResource(id = R.color.textDark), fontSize = 14.sp, - modifier = Modifier.testTag("checkpointDueDate") + modifier = Modifier.testTag("checkpointDueDate_${discussionCheckpointUiState.name}") ) if (discussionCheckpointUiState.submissionStateLabel != SubmissionStateLabel.None) { Spacer(modifier = Modifier.width(4.dp)) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/ComposeCanvasWebViewWrapper.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/ComposeCanvasWebViewWrapper.kt index 700a93eaab..442a44cccd 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/ComposeCanvasWebViewWrapper.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/ComposeCanvasWebViewWrapper.kt @@ -21,8 +21,11 @@ import android.webkit.WebView import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.viewinterop.AndroidView import androidx.core.os.bundleOf @@ -47,6 +50,13 @@ fun ComposeCanvasWebViewWrapper( embeddedWebViewCallbacks: ComposeEmbeddedWebViewCallbacks? = null, ) { val webViewState = rememberSaveable(content) { bundleOf() } + val savedHtml = rememberSaveable(content, stateSaver = Saver( + save = { it }, + restore = { it } + )) { mutableStateOf(content) } + val savedThemeSwitched = rememberSaveable { bundleOf("themeSwitched" to false) } + val configuration = LocalConfiguration.current + val configKey = "${configuration.orientation}-${configuration.uiMode}" if (LocalInspectionMode.current) { Text(text = content) @@ -84,27 +94,34 @@ fun ComposeCanvasWebViewWrapper( applyOnWebView?.let { applyOnWebView -> webView.applyOnWebView() } } }, - update = { + update = { view -> + configKey // Read configuration to trigger update on change + savedHtml.value = content // Update saved HTML on each update if (webViewState.isEmpty) { if (useInAppFormatting) { - it.loadHtml(content, title) + view.loadHtml(savedHtml.value, title) } else { - it.loadDataWithBaseUrl(CanvasWebView.getReferrer(true), content, contentType, "UTF-8", null) + view.loadDataWithBaseUrl(CanvasWebView.getReferrer(true), savedHtml.value, contentType, "UTF-8", null) } if (onLtiButtonPressed != null) { - it.webView.addJavascriptInterface(JsExternalToolInterface(onLtiButtonPressed), Const.LTI_TOOL) + view.webView.addJavascriptInterface(JsExternalToolInterface(onLtiButtonPressed), Const.LTI_TOOL) } - if (HtmlContentFormatter.hasGoogleDocsUrl(content)) { - it.webView.addJavascriptInterface(JsGoogleDocsInterface(it.context), Const.GOOGLE_DOCS) + if (HtmlContentFormatter.hasGoogleDocsUrl(savedHtml.value)) { + view.webView.addJavascriptInterface(JsGoogleDocsInterface(view.context), Const.GOOGLE_DOCS) } + view.handleConfigurationChange() } else { - it.webView.restoreState(webViewState) + view.webView.restoreState(webViewState) + view.setHtmlContent(savedHtml.value) + view.setThemeSwitched(savedThemeSwitched.getBoolean("themeSwitched", false)) + view.handleConfigurationChange(reloadContent = false) } - applyOnUpdate?.let { applyOnUpdate -> it.applyOnUpdate() } + applyOnUpdate?.let { applyOnUpdate -> view.applyOnUpdate() } }, onRelease = { + savedThemeSwitched.putBoolean("themeSwitched", it.themeSwitched) it.webView.saveState(webViewState) }, modifier = modifier.fillMaxSize() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/TriStateBottomSheet.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/TriStateBottomSheet.kt index 63086fa181..fbabb10864 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/TriStateBottomSheet.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/TriStateBottomSheet.kt @@ -135,8 +135,8 @@ fun TriStateBottomSheet( anchoredDraggableState.updateAnchors(newAnchors) val currentTarget = anchoredDraggableState.targetValue - if (!newAnchors.hasAnchorFor(currentTarget)) { - if (newAnchors.hasAnchorFor(initialAnchor)) { + if (!newAnchors.hasPositionFor(currentTarget)) { + if (newAnchors.hasPositionFor(initialAnchor)) { anchoredDraggableState.snapTo(initialAnchor) } else if (newAnchors.size > 0) { newAnchors.closestAnchor(anchoredDraggableState.offset.takeIf { !it.isNaN() } @@ -153,7 +153,7 @@ fun TriStateBottomSheet( LaunchedEffect(anchoredDraggableState.anchors, initialAnchor) { val currentAnchors = anchoredDraggableState.anchors - if (currentAnchors.hasAnchorFor(initialAnchor) && anchoredDraggableState.currentValue != initialAnchor) { + if (currentAnchors.hasPositionFor(initialAnchor) && anchoredDraggableState.currentValue != initialAnchor) { if (anchoredDraggableState.targetValue != initialAnchor || !anchoredDraggableState.isAnimationRunning) { anchoredDraggableState.snapTo(initialAnchor) } @@ -168,8 +168,8 @@ fun TriStateBottomSheet( if (offset.isNaN() || currentAnchors.size == 0) { getPixelForAnchor(initialAnchor) } else { - val minAnchorValue = currentAnchors.minAnchor() - val maxAnchorValue = currentAnchors.maxAnchor() + val minAnchorValue = currentAnchors.minPosition() + val maxAnchorValue = currentAnchors.maxPosition() offset.coerceIn(minAnchorValue, maxAnchorValue) } } @@ -259,7 +259,7 @@ fun TriStateBottomSheet( coroutineScope.launch { when (anchoredDraggableState.targetValue) { AnchorPoints.BOTTOM -> { - if (anchoredDraggableState.anchors.hasAnchorFor(AnchorPoints.MIDDLE)) { + if (anchoredDraggableState.anchors.hasPositionFor(AnchorPoints.MIDDLE)) { anchoredDraggableState.animateTo(AnchorPoints.MIDDLE) } else { anchoredDraggableState.animateTo(AnchorPoints.BOTTOM) @@ -271,7 +271,7 @@ fun TriStateBottomSheet( } AnchorPoints.TOP -> { - if (anchoredDraggableState.anchors.hasAnchorFor(AnchorPoints.MIDDLE)) { + if (anchoredDraggableState.anchors.hasPositionFor(AnchorPoints.MIDDLE)) { anchoredDraggableState.animateTo(AnchorPoints.MIDDLE) } else { anchoredDraggableState.animateTo(AnchorPoints.BOTTOM) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt index 427de8957f..dbea27770a 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt @@ -36,6 +36,7 @@ import com.instructure.pandautils.room.offline.daos.FileSyncSettingsDao import com.instructure.pandautils.room.offline.daos.LocalFileDao import com.instructure.pandautils.typeface.TypefaceBehavior import com.instructure.pandautils.utils.ColorKeeper +import com.instructure.pandautils.utils.FeatureFlagProvider import com.instructure.pandautils.utils.HtmlContentFormatter import com.instructure.pandautils.utils.LocaleUtils import com.instructure.pandautils.utils.StorageUtils @@ -82,9 +83,10 @@ class ApplicationModule { fun provideHtmlContentFormatter( @ApplicationContext context: Context, oAuthManager: OAuthManager, - firebaseCrashlytics: FirebaseCrashlytics + firebaseCrashlytics: FirebaseCrashlytics, + featureFlagProvider: FeatureFlagProvider ): HtmlContentFormatter { - return HtmlContentFormatter(context, firebaseCrashlytics, oAuthManager) + return HtmlContentFormatter(context, firebaseCrashlytics, oAuthManager, featureFlagProvider ) } @Provides diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/AssignmentSubmissionModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/AssignmentSubmissionModule.kt index 9d67806ed6..f7cdb2c0e7 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/AssignmentSubmissionModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/AssignmentSubmissionModule.kt @@ -20,6 +20,7 @@ import com.instructure.canvasapi2.apis.CourseAPI import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.apis.SectionAPI import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager +import com.instructure.canvasapi2.managers.graphql.DifferentiationTagsManager import com.instructure.pandautils.features.speedgrader.AssignmentSubmissionRepository import dagger.Module import dagger.Provides @@ -29,14 +30,16 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) class AssignmentSubmissionModule { + @Provides fun provideAssignmentSubmissionListRepository( assignmentApi: AssignmentAPI.AssignmentInterface, enrollmentApi: EnrollmentAPI.EnrollmentInterface, courseApi: CourseAPI.CoursesInterface, sectionApi: SectionAPI.SectionsInterface, - customGradeStatusesManager: CustomGradeStatusesManager + customGradeStatusesManager: CustomGradeStatusesManager, + differentiationTagsManager: DifferentiationTagsManager ): AssignmentSubmissionRepository { - return AssignmentSubmissionRepository(assignmentApi, enrollmentApi, courseApi, sectionApi, customGradeStatusesManager) + return AssignmentSubmissionRepository(assignmentApi, enrollmentApi, courseApi, sectionApi, customGradeStatusesManager, differentiationTagsManager) } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/DifferentiationTagsModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/DifferentiationTagsModule.kt new file mode 100644 index 0000000000..30543c5853 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/DifferentiationTagsModule.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.pandautils.di + +import com.apollographql.apollo.ApolloClient +import com.instructure.canvasapi2.di.DefaultApolloClient +import com.instructure.canvasapi2.managers.graphql.DifferentiationTagsManager +import com.instructure.canvasapi2.managers.graphql.DifferentiationTagsManagerImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class DifferentiationTagsModule { + + @Provides + fun provideDifferentiationTagsManager(@DefaultApolloClient apolloClient: ApolloClient): DifferentiationTagsManager { + return DifferentiationTagsManagerImpl(apolloClient) + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/FileDownloaderModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/FileDownloaderModule.kt index 3a48e18af5..bd7fac87ad 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/FileDownloaderModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/FileDownloaderModule.kt @@ -17,18 +17,31 @@ package com.instructure.pandautils.di import android.content.Context import android.webkit.CookieManager +import com.instructure.pandautils.utils.DownloadNotificationHelper import com.instructure.pandautils.utils.FileDownloader import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) class FileDownloaderModule { + + @Provides + @Singleton + fun provideDownloadNotificationHelper(@ApplicationContext context: Context): DownloadNotificationHelper { + return DownloadNotificationHelper(context) + } + @Provides - fun provideFileDownloader(@ApplicationContext context: Context, cookieManager: CookieManager): FileDownloader { - return FileDownloader(context, cookieManager) + fun provideFileDownloader( + @ApplicationContext context: Context, + cookieManager: CookieManager, + downloadNotificationHelper: DownloadNotificationHelper + ): FileDownloader { + return FileDownloader(context, cookieManager, downloadNotificationHelper) } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt index 4f4433f3ed..313d9fe746 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt @@ -30,6 +30,7 @@ import com.instructure.pandautils.room.offline.daos.AssignmentScoreStatisticsDao import com.instructure.pandautils.room.offline.daos.AssignmentSetDao import com.instructure.pandautils.room.offline.daos.AttachmentDao import com.instructure.pandautils.room.offline.daos.AuthorDao +import com.instructure.pandautils.room.offline.daos.CheckpointDao import com.instructure.pandautils.room.offline.daos.ConferenceDao import com.instructure.pandautils.room.offline.daos.ConferenceRecodingDao import com.instructure.pandautils.room.offline.daos.CourseDao @@ -67,6 +68,7 @@ import com.instructure.pandautils.room.offline.daos.ModuleItemDao import com.instructure.pandautils.room.offline.daos.ModuleNameDao import com.instructure.pandautils.room.offline.daos.ModuleObjectDao import com.instructure.pandautils.room.offline.daos.PageDao +import com.instructure.pandautils.room.offline.daos.PlannerItemDao import com.instructure.pandautils.room.offline.daos.PlannerOverrideDao import com.instructure.pandautils.room.offline.daos.QuizDao import com.instructure.pandautils.room.offline.daos.RemoteFileDao @@ -78,6 +80,7 @@ import com.instructure.pandautils.room.offline.daos.ScheduleItemAssignmentOverri import com.instructure.pandautils.room.offline.daos.ScheduleItemDao import com.instructure.pandautils.room.offline.daos.SectionDao import com.instructure.pandautils.room.offline.daos.StudioMediaProgressDao +import com.instructure.pandautils.room.offline.daos.SubAssignmentSubmissionDao import com.instructure.pandautils.room.offline.daos.SubmissionCommentDao import com.instructure.pandautils.room.offline.daos.SubmissionDao import com.instructure.pandautils.room.offline.daos.SyncSettingsDao @@ -200,6 +203,11 @@ class OfflineModule { return appDatabase.groupDao() } + @Provides + fun providePlannerItemDao(appDatabase: OfflineDatabase): PlannerItemDao { + return appDatabase.plannerItemDao() + } + @Provides fun providePlannerOverrideDao(appDatabase: OfflineDatabase): PlannerOverrideDao { return appDatabase.plannerOverrideDao() @@ -318,6 +326,7 @@ class OfflineModule { lockInfoFacade: LockInfoFacade, rubricCriterionRatingDao: RubricCriterionRatingDao, assignmentRubricCriterionDao: AssignmentRubricCriterionDao, + checkpointDao: CheckpointDao, offlineDatabase: OfflineDatabase ): AssignmentFacade { return AssignmentFacade( @@ -332,6 +341,7 @@ class OfflineModule { lockInfoFacade, rubricCriterionRatingDao, assignmentRubricCriterionDao, + checkpointDao, offlineDatabase ) } @@ -345,11 +355,13 @@ class OfflineModule { submissionCommentDao: SubmissionCommentDao, attachmentDao: AttachmentDao, authorDao: AuthorDao, - rubricCriterionAssessmentDao: RubricCriterionAssessmentDao + rubricCriterionAssessmentDao: RubricCriterionAssessmentDao, + subAssignmentSubmissionDao: SubAssignmentSubmissionDao ): SubmissionFacade { return SubmissionFacade( submissionDao, groupDao, mediaCommentDao, userDao, - submissionCommentDao, attachmentDao, authorDao, rubricCriterionAssessmentDao + submissionCommentDao, attachmentDao, authorDao, rubricCriterionAssessmentDao, + subAssignmentSubmissionDao ) } @@ -362,6 +374,8 @@ class OfflineModule { localFileDao: LocalFileDao, discussionTopicRemoteFileDao: DiscussionTopicRemoteFileDao, offlineDatabase: OfflineDatabase, + assignmentDao: AssignmentDao, + checkpointDao: CheckpointDao ): DiscussionTopicHeaderFacade { return DiscussionTopicHeaderFacade( discussionTopicHeaderDao, @@ -370,7 +384,9 @@ class OfflineModule { remoteFileDao, localFileDao, discussionTopicRemoteFileDao, - offlineDatabase + offlineDatabase, + assignmentDao, + checkpointDao ) } @@ -436,12 +452,12 @@ class OfflineModule { @Provides fun provideScheduleItemFacade( scheduleItemDao: ScheduleItemDao, - assignmentDao: AssignmentDao, assignmentOverrideDao: AssignmentOverrideDao, scheduleItemAssignmentOverrideDao: ScheduleItemAssignmentOverrideDao, + assignmentFacade: AssignmentFacade, offlineDatabase: OfflineDatabase ): ScheduleItemFacade { - return ScheduleItemFacade(scheduleItemDao, assignmentOverrideDao, scheduleItemAssignmentOverrideDao, assignmentDao, offlineDatabase) + return ScheduleItemFacade(scheduleItemDao, assignmentOverrideDao, scheduleItemAssignmentOverrideDao, assignmentFacade, offlineDatabase) } @Provides @@ -643,4 +659,14 @@ class OfflineModule { fun provideCustomGradeStatusDao(database: OfflineDatabase): CustomGradeStatusDao { return database.customGradeStatusDao() } + + @Provides + fun provideCheckpointDao(database: OfflineDatabase): CheckpointDao { + return database.checkpointDao() + } + + @Provides + fun provideSubAssignmentSubmissionDao(database: OfflineDatabase): SubAssignmentSubmissionDao { + return database.subAssignmentSubmissionDao() + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt index 8b9e24ac21..6b23c3c76e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt @@ -33,10 +33,12 @@ import com.instructure.canvasapi2.apis.GroupAPI import com.instructure.canvasapi2.apis.LaunchDefinitionsAPI import com.instructure.canvasapi2.apis.ModuleAPI import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.canvasapi2.apis.PlannerAPI import com.instructure.canvasapi2.apis.QuizAPI import com.instructure.canvasapi2.apis.StudioApi import com.instructure.canvasapi2.apis.UserAPI import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager +import com.instructure.canvasapi2.managers.graphql.ModuleManager import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.features.offline.offlinecontent.CourseFileSharedRepository import com.instructure.pandautils.features.offline.sync.AggregateProgressObserver @@ -44,6 +46,7 @@ import com.instructure.pandautils.features.offline.sync.CourseSync import com.instructure.pandautils.features.offline.sync.FileSync import com.instructure.pandautils.features.offline.sync.HtmlParser import com.instructure.pandautils.features.offline.sync.StudioSync +import com.instructure.pandautils.room.offline.daos.CheckpointDao import com.instructure.pandautils.room.offline.daos.CourseFeaturesDao import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao @@ -53,6 +56,7 @@ import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao import com.instructure.pandautils.room.offline.daos.FileSyncSettingsDao import com.instructure.pandautils.room.offline.daos.LocalFileDao import com.instructure.pandautils.room.offline.daos.PageDao +import com.instructure.pandautils.room.offline.daos.PlannerItemDao import com.instructure.pandautils.room.offline.daos.QuizDao import com.instructure.pandautils.room.offline.daos.StudioMediaProgressDao import com.instructure.pandautils.room.offline.facade.AssignmentFacade @@ -148,7 +152,11 @@ class OfflineSyncModule { firebaseCrashlytics: FirebaseCrashlytics, fileSync: FileSync, customGradeStatusDao: CustomGradeStatusDao, - customGradeStatusesManager: CustomGradeStatusesManager + customGradeStatusesManager: CustomGradeStatusesManager, + plannerApi: PlannerAPI.PlannerInterface, + plannerItemDao: PlannerItemDao, + checkpointDao: CheckpointDao, + moduleManager: ModuleManager ): CourseSync { return CourseSync( courseApi, @@ -156,6 +164,7 @@ class OfflineSyncModule { userApi, assignmentApi, calendarEventApi, + plannerApi, courseSyncSettingsDao, pageFacade, userFacade, @@ -186,7 +195,10 @@ class OfflineSyncModule { firebaseCrashlytics, fileSync, customGradeStatusDao, - customGradeStatusesManager + customGradeStatusesManager, + plannerItemDao, + checkpointDao, + moduleManager ) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt index 0d9d3fbd64..af6b557589 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsFragment.kt @@ -33,7 +33,6 @@ import android.view.ViewGroup import android.webkit.WebView import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.google.android.material.snackbar.Snackbar @@ -53,7 +52,8 @@ import com.instructure.pandautils.analytics.SCREEN_VIEW_ASSIGNMENT_DETAILS import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.base.BaseCanvasFragment import com.instructure.pandautils.databinding.FragmentAssignmentDetailsBinding -import com.instructure.pandautils.features.reminder.composables.ReminderView +import com.instructure.pandautils.features.assignments.details.composables.DiscussionCheckpointLayout +import com.instructure.pandautils.features.assignments.details.composables.DueDateReminderLayout import com.instructure.pandautils.features.shareextension.ShareFileSubmissionTarget import com.instructure.pandautils.navigation.WebViewRouter import com.instructure.pandautils.utils.Const @@ -93,6 +93,8 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo @get:PageViewUrlParam(name = "courseId") val courseId by LongArg(key = Const.COURSE_ID, default = 0) + val submissionId by LongArg(key = Const.SUBMISSION_ID, default = -1) + private var binding: FragmentAssignmentDetailsBinding? = null private val viewModel: AssignmentDetailsViewModel by viewModels() @@ -101,7 +103,8 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo val assignment = viewModel.assignment val course = viewModel.course.value if (assignment != null && captureVideoUri != null && it && course != null) { - assignmentDetailsRouter.navigateToAssignmentUploadPicker(requireActivity(), course, assignment, captureVideoUri!!) + val nextAttempt = (assignment.submission?.attempt ?: 0) + 1 + assignmentDetailsRouter.navigateToAssignmentUploadPicker(requireActivity(), course, assignment, captureVideoUri!!, nextAttempt.toLong(), "camera") } else { toast(R.string.videoRecordingError) } @@ -111,7 +114,8 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo val assignment = viewModel.assignment val course = viewModel.course.value if (assignment != null && it != null && course != null) { - assignmentDetailsRouter.navigateToAssignmentUploadPicker(requireActivity(), course, assignment, it) + val nextAttempt = (assignment.submission?.attempt ?: 0) + 1 + assignmentDetailsRouter.navigateToAssignmentUploadPicker(requireActivity(), course, assignment, it, nextAttempt.toLong(), "library") } else { toast(R.string.unexpectedErrorOpeningFile) } @@ -144,16 +148,27 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo viewModel.course.value?.let { viewModel.updateReminderColor(assignmentDetailsBehaviour.getThemeColor(it)) } - binding?.reminderComposeView?.setContent { - val state by viewModel.reminderViewState.collectAsState() - ReminderView( - viewState = state, - onAddClick = { checkAlarmPermission() }, + + binding?.dueComposeView?.setContent { + val states = viewModel.dueDateReminderViewStates + DueDateReminderLayout( + states, + onAddClick = { tag -> checkAlarmPermission(tag) }, onRemoveClick = { reminderId -> - viewModel.showDeleteReminderConfirmationDialog(requireContext(), reminderId, assignmentDetailsBehaviour.dialogColor) + viewModel.showDeleteReminderConfirmationDialog( + requireContext(), + reminderId, + assignmentDetailsBehaviour.dialogColor + ) } ) } + + binding?.checkpointGradesComposeView?.setContent { + val checkpoints = viewModel.discussionCheckpoints.collectAsState() + DiscussionCheckpointLayout(checkpoints.value) + } + return binding?.root } @@ -213,7 +228,8 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo action.assignmentUrl, action.isAssignmentEnhancementEnabled, action.isObserver, - action.selectedSubmissionAttempt + action.selectedSubmissionAttempt, + action.isQuiz ) } is AssignmentDetailAction.NavigateToQuizScreen -> { @@ -365,14 +381,14 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo } } - private fun checkAlarmPermission() { + private fun checkAlarmPermission(tag: String? = null) { val alarmManager = context?.getSystemService(Context.ALARM_SERVICE) as AlarmManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && requireActivity().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { viewModel.checkingNotificationPermission = true notificationsPermissionContract.launch(Manifest.permission.POST_NOTIFICATIONS) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (alarmManager.canScheduleExactAlarms()) { - viewModel.showCreateReminderDialog(requireActivity(), assignmentDetailsBehaviour.dialogColor) + viewModel.showCreateReminderDialog(requireActivity(), assignmentDetailsBehaviour.dialogColor, tag) } else { viewModel.checkingReminderPermission = true startActivity( @@ -383,7 +399,7 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo ) } } else { - viewModel.showCreateReminderDialog(requireActivity(), assignmentDetailsBehaviour.dialogColor) + viewModel.showCreateReminderDialog(requireActivity(), assignmentDetailsBehaviour.dialogColor, tag) } } @@ -432,7 +448,7 @@ class AssignmentDetailsFragment : BaseCanvasFragment(), FragmentInteractions, Bo if (route.paramsHash.containsKey(RouterParams.SUBMISSION_ID)) { // Indicate that we want to route to the Submission Details page - this will give us a small backstack, allowing the user to hit back and go to Assignment Details instead // of closing the app (in the case of when the app isn't running and the user hits a push notification that takes them to Submission Details) - route.arguments.putString(Const.SUBMISSION_ID, route.paramsHash[RouterParams.SUBMISSION_ID]) + route.arguments.putLong(Const.SUBMISSION_ID, route.paramsHash[RouterParams.SUBMISSION_ID]?.toLong().orDefault()) } if (route.paramsHash.containsKey(RouterParams.COURSE_ID)) { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsRouter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsRouter.kt index 4f76e64761..f598dbf486 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsRouter.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsRouter.kt @@ -30,7 +30,9 @@ open class AssignmentDetailsRouter { activity: FragmentActivity, canvasContext: CanvasContext, assignment: Assignment, - mediaUri: Uri + mediaUri: Uri, + attempt: Long = 1L, + mediaSource: String? = null ) = Unit open fun navigateToSubmissionScreen( @@ -40,7 +42,8 @@ open class AssignmentDetailsRouter { assignmentUrl: String?, isAssignmentEnhancementEnabled: Boolean, isObserver: Boolean = false, - initialSelectedSubmissionAttempt: Long? = null + initialSelectedSubmissionAttempt: Long? = null, + isQuiz: Boolean = false ) = Unit open fun navigateToQuizScreen(activity: FragmentActivity, canvasContext: CanvasContext, quiz: Quiz, url: String) = Unit @@ -56,7 +59,8 @@ open class AssignmentDetailsRouter { activity: FragmentActivity, canvasContext: CanvasContext, assignment: Assignment, - attemptId: Long? = null + attemptId: Long? = null, + attempt: Long = 1L ) = Unit open fun navigateToTextEntryScreen( @@ -65,7 +69,8 @@ open class AssignmentDetailsRouter { assignmentId: Long, assignmentName: String? = "", initialText: String? = null, - isFailure: Boolean = false + isFailure: Boolean = false, + attempt: Long = 1L ) = Unit open fun navigateToUrlSubmissionScreen( @@ -74,7 +79,8 @@ open class AssignmentDetailsRouter { assignmentId: Long, assignmentName: String? = "", initialUrl: String?, - isFailure: Boolean = false + isFailure: Boolean = false, + attempt: Long = 1L ) = Unit open fun navigateToAnnotationSubmissionScreen( @@ -83,7 +89,8 @@ open class AssignmentDetailsRouter { annotatableAttachmentId: Long, submissionId: Long, assignmentId: Long, - assignmentName: String + assignmentName: String, + attempt: Long = 1L ) = Unit open fun navigateToLtiLaunchScreen( diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt index c5a904087d..1a990e04bc 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewData.kt @@ -40,8 +40,7 @@ data class AssignmentDetailsViewData( val discussionHeaderViewData: DiscussionHeaderViewData? = null, val quizDetails: QuizViewViewData? = null, val attemptsViewData: AttemptsViewData? = null, - @Bindable var hasDraft: Boolean = false, - @Bindable var reminders: List = emptyList() + @Bindable var hasDraft: Boolean = false ) : BaseObservable() { val firstAttemptOrNull = attempts.firstOrNull() val noDescriptionVisible = description.isEmpty() && !fullLocked @@ -70,7 +69,8 @@ sealed class AssignmentDetailAction { val isObserver: Boolean, val selectedSubmissionAttempt: Long?, val assignmentUrl: String?, - val isAssignmentEnhancementEnabled: Boolean + val isAssignmentEnhancementEnabled: Boolean, + val isQuiz: Boolean ) : AssignmentDetailAction() data class NavigateToQuizScreen(val quiz: Quiz) : AssignmentDetailAction() data class NavigateToDiscussionScreen(val discussionTopicHeaderId: Long, val course: Course) : AssignmentDetailAction() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt index ff70bf6b11..12b26fb34d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt @@ -22,6 +22,7 @@ import android.content.Context import android.content.res.Resources import android.net.Uri import androidx.annotation.ColorInt +import androidx.compose.runtime.mutableStateListOf import androidx.compose.ui.graphics.Color import androidx.fragment.app.FragmentActivity import androidx.lifecycle.LiveData @@ -56,6 +57,7 @@ import com.instructure.pandautils.features.assignmentdetails.AssignmentDetailsAt import com.instructure.pandautils.features.assignmentdetails.AssignmentDetailsAttemptViewData import com.instructure.pandautils.features.assignments.details.gradecellview.GradeCellViewData import com.instructure.pandautils.features.assignments.details.itemviewmodels.ReminderItemViewModel +import com.instructure.pandautils.features.grades.SubmissionStateLabel import com.instructure.pandautils.features.reminder.ReminderItem import com.instructure.pandautils.features.reminder.ReminderManager import com.instructure.pandautils.features.reminder.ReminderViewState @@ -65,14 +67,18 @@ import com.instructure.pandautils.room.appdatabase.entities.ReminderEntity import com.instructure.pandautils.utils.AssignmentUtils2 import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.HtmlContentFormatter +import com.instructure.pandautils.utils.getSubAssignmentSubmissionGrade +import com.instructure.pandautils.utils.getSubAssignmentSubmissionStateLabel +import com.instructure.pandautils.utils.getSubmissionStateLabel import com.instructure.pandautils.utils.isAudioVisualExtension import com.instructure.pandautils.utils.orDefault +import com.instructure.pandautils.utils.orderedCheckpoints import com.instructure.pandautils.utils.toFormattedString import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.io.File import java.text.DateFormat @@ -112,6 +118,7 @@ class AssignmentDetailsViewModel @Inject constructor( private val _course = MutableLiveData(Course(id = courseId)) private val assignmentId = savedStateHandle.get(Const.ASSIGNMENT_ID).orDefault() + private val submissionId = savedStateHandle.get(Const.SUBMISSION_ID) var bookmarker = Bookmarker(true, course.value).withParam(RouterParams.ASSIGNMENT_ID, assignmentId.toString()) @@ -131,8 +138,14 @@ class AssignmentDetailsViewModel @Inject constructor( private var selectedSubmission: Submission? = null - private val _reminderViewState = MutableStateFlow(ReminderViewState()) - val reminderViewState = _reminderViewState.asStateFlow() + private var reminderEntities: List = emptyList() + private var themeColor: Color? = null + private val _dueDateReminderViewStates = mutableStateListOf() + val dueDateReminderViewStates: List + get() = _dueDateReminderViewStates + + private val _discussionCheckpoints = MutableStateFlow>(emptyList()) + val discussionCheckpoints: StateFlow> = _discussionCheckpoints.asStateFlow() var checkingReminderPermission = false var checkingNotificationPermission = false @@ -153,15 +166,29 @@ class AssignmentDetailsViewModel @Inject constructor( loadData() reminderManager.observeRemindersLiveData(apiPrefs.user?.id.orDefault(), assignmentId) { reminderEntities -> - _data.value?.reminders = mapReminders(reminderEntities) - _reminderViewState.update { it.copy( - reminders = reminderEntities.map { ReminderItem(it.id, it.text, Date(it.time)) }, - dueDate = assignment?.dueDate - ) } - _data.value?.notifyPropertyChanged(BR.reminders) + this.reminderEntities = reminderEntities + updateDueDatesViewState(reminderEntities) } } + private fun updateDueDatesViewState(reminderEntities: List) { + for (i in 0.._dueDateReminderViewStates.lastIndex) { + val tag = _dueDateReminderViewStates[i].tag + _dueDateReminderViewStates[i] = _dueDateReminderViewStates[i].copy( + reminders = getReminderItems(tag) + ) + } + } + + private fun getReminderItems(tag: String? = null): List { + return reminderEntities + .filter { it.tag == tag } + .sortedBy { it.time } + .map { + ReminderItem(it.id, it.text, Date(it.time)) + } + } + fun getVideoUri(fragment: FragmentActivity): Uri? = submissionHandler.getVideoUri(fragment) override fun onCleared() { @@ -224,11 +251,96 @@ class AssignmentDetailsViewModel @Inject constructor( isAssignmentEnhancementEnabled = assignmentDetailsRepository.isAssignmentEnhancementEnabled(courseId.orDefault(), forceNetwork) assignment = assignmentResult - _reminderViewState.update { it.copy( - dueDate = if (assignment?.submission?.excused.orDefault()) null else assignment?.dueDate - ) } + + if (assignment?.checkpoints?.isNotEmpty() == true) { + _dueDateReminderViewStates.clear() + val checkpointsList = mutableListOf() + assignment?.orderedCheckpoints?.forEach { checkpoint -> + val dueLabel = when (checkpoint.tag) { + Const.REPLY_TO_TOPIC -> application.getString(R.string.reply_to_topic_due) + Const.REPLY_TO_ENTRY -> { + application.getString( + R.string.additional_replies_due, + assignment?.discussionTopicHeader?.replyRequiredCount ?: 0 + ) + } + + else -> application.getString(R.string.dueLabel) + } + val subAssignment = assignment?.submission?.subAssignmentSubmissions?.firstOrNull { it.subAssignmentTag == checkpoint.tag } + _dueDateReminderViewStates.add( + ReminderViewState( + dueLabel = dueLabel, + themeColor = themeColor, + dueDate = if (subAssignment?.excused.orDefault()) null else checkpoint.dueDate, + tag = checkpoint.tag, + reminders = getReminderItems(checkpoint.tag) + ) + ) + + val checkpointName = when (checkpoint.tag) { + Const.REPLY_TO_TOPIC -> application.getString(R.string.reply_to_topic) + Const.REPLY_TO_ENTRY -> application.getString( + R.string.additional_replies, + assignment?.discussionTopicHeader?.replyRequiredCount.orDefault() + ) + else -> checkpoint.name.orEmpty() + } + val stateLabel = assignmentResult.getSubAssignmentSubmissionStateLabel(subAssignment, customStatuses) + val grade = assignmentResult.getSubAssignmentSubmissionGrade( + checkpoint.pointsPossible.orDefault(), + subAssignment, + resources, + restrictQuantitativeData, + gradingScheme, + showZeroPossiblePoints = true, + showNotGraded = true + ) + checkpointsList.add( + DiscussionCheckpointViewState( + name = checkpointName, + stateLabel = stateLabel, + grade = grade.text, + courseColor = assignmentDetailsColorProvider.getContentColor(course.value).color() + ) + ) + } + _discussionCheckpoints.value = checkpointsList + } else { + _dueDateReminderViewStates.clear() + _discussionCheckpoints.value = emptyList() + _dueDateReminderViewStates.add( + ReminderViewState( + dueLabel = application.getString(R.string.dueLabel), + themeColor = themeColor, + dueDate = if (assignment?.submission?.excused.orDefault()) null else assignment?.dueDate, + tag = null, + reminders = getReminderItems() + ) + ) + } _data.postValue(getViewData(assignmentResult, hasDraft)) _state.postValue(ViewState.Success) + + // Check if we need to auto-navigate to submission details from push notification + submissionId?.let { subId -> + val submission = assignmentResult.submission + if (submission != null + && submission.id == subId + && submission.submissionType != SubmissionType.NOT_GRADED.apiString + && submission.submissionType != SubmissionType.ON_PAPER.apiString) + { + postAction( + AssignmentDetailAction.NavigateToSubmissionScreen( + isObserver, + submission.attempt, + assignmentResult.htmlUrl, + isAssignmentEnhancementEnabled, + assignmentResult.isQuiz() + ) + ) + } + } } catch (ex: Exception) { val errorString = if (ex is IllegalAccessException) { resources.getString(R.string.assignmentNoLongerAvailable) @@ -263,35 +375,15 @@ class AssignmentDetailsViewModel @Inject constructor( } val assignmentState = AssignmentUtils2.getAssignmentState(assignment, assignment.submission, false) - - // Don't mark LTI assignments as missing when overdue as they usually won't have a real submission for it - val isMissing = assignment.isMissing() || (assignment.turnInType != Assignment.TurnInType.EXTERNAL_TOOL - && assignment.dueAt != null - && assignmentState == AssignmentUtils2.ASSIGNMENT_STATE_MISSING) - - val matchedCustomStatus = assignment.submission?.customGradeStatusId?.let { id -> - customStatuses.find { it._id.toLongOrNull() == id } - } - - val submittedLabelText = when { - matchedCustomStatus != null -> matchedCustomStatus.name - isMissing -> resources.getString(R.string.missingAssignment) - !assignment.isSubmitted -> resources.getString(R.string.notSubmitted) - assignment.isGraded() -> resources.getString(R.string.gradedSubmissionLabel) - else -> resources.getString(R.string.submitted) - } - - val submissionStatusTint = when { - matchedCustomStatus != null -> R.color.textInfo - assignment.isSubmitted -> R.color.textSuccess - isMissing -> R.color.textDanger - else -> R.color.textDark - } - - val submittedStatusIcon = when { - matchedCustomStatus != null -> R.drawable.ic_flag - assignment.isSubmitted -> R.drawable.ic_complete_solid - else -> R.drawable.ic_no + val submissionStateLabel = assignment + .getSubmissionStateLabel(customStatuses) + .takeUnless { it == SubmissionStateLabel.None } + ?: SubmissionStateLabel.Submitted + val submissionStatusTint = submissionStateLabel.colorRes + val submittedStatusIcon = submissionStateLabel.iconRes + val submittedLabelText = when (submissionStateLabel) { + is SubmissionStateLabel.Predefined -> resources.getString(submissionStateLabel.labelRes) + is SubmissionStateLabel.Custom -> submissionStateLabel.label } // Submission Status under title - We only show Graded or nothing at all for PAPER/NONE @@ -420,7 +512,8 @@ class AssignmentDetailsViewModel @Inject constructor( "${it}" } else { it - } + }, + courseId ) }.orEmpty() @@ -476,8 +569,7 @@ class AssignmentDetailsViewModel @Inject constructor( discussionHeaderViewData = discussionHeaderViewData, quizDetails = quizViewViewData, attemptsViewData = attemptsViewData, - hasDraft = hasDraft, - reminders = _data.value?.reminders.orEmpty(), + hasDraft = hasDraft ) } @@ -545,7 +637,8 @@ class AssignmentDetailsViewModel @Inject constructor( isObserver, selectedSubmission?.attempt, assignment?.htmlUrl, - isAssignmentEnhancementEnabled + isAssignmentEnhancementEnabled, + assignment?.isQuiz() == true ) ) } @@ -626,28 +719,34 @@ class AssignmentDetailsViewModel @Inject constructor( } fun updateReminderColor(@ColorInt color: Int) { - _reminderViewState.update { it.copy(themeColor = Color(color)) } + themeColor = Color(color) + for (i in 0.._dueDateReminderViewStates.lastIndex) { + _dueDateReminderViewStates[i] = _dueDateReminderViewStates[i].copy(themeColor = themeColor) + } } - fun showCreateReminderDialog(context: Context, @ColorInt color: Int) { + fun showCreateReminderDialog(context: Context, @ColorInt color: Int, tag: String? = null) { assignment?.let { assignment -> viewModelScope.launch { + val dueDate = _dueDateReminderViewStates.firstOrNull { it.tag == tag }?.dueDate when { - assignment.dueDate == null -> reminderManager.showCustomReminderDialog( + dueDate == null -> reminderManager.showCustomReminderDialog( context, apiPrefs.user?.id.orDefault(), assignment.id, assignment.name.orEmpty(), assignment.htmlUrl.orEmpty(), - assignment.dueDate + dueDate, + tag ) - assignment.dueDate?.before(Date()).orDefault() -> reminderManager.showCustomReminderDialog( + dueDate.before(Date()).orDefault() -> reminderManager.showCustomReminderDialog( context, apiPrefs.user?.id.orDefault(), assignment.id, assignment.name.orEmpty(), assignment.htmlUrl.orEmpty(), - assignment.dueDate + dueDate, + tag ) else -> reminderManager.showBeforeDueDateReminderDialog( context, @@ -655,8 +754,9 @@ class AssignmentDetailsViewModel @Inject constructor( assignment.id, assignment.name.orEmpty(), assignment.htmlUrl.orEmpty(), - assignment.dueDate ?: Date(), - color + dueDate ?: Date(), + color, + tag ) } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginFindSchoolPageTest.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/DiscussionCheckpointViewState.kt similarity index 56% rename from apps/student/src/androidTest/java/com/instructure/student/ui/LoginFindSchoolPageTest.kt rename to libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/DiscussionCheckpointViewState.kt index 3e59511e4a..392b28fd99 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginFindSchoolPageTest.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/DiscussionCheckpointViewState.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 - present Instructure, Inc. + * Copyright (C) 2025 - present Instructure, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,21 +12,14 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ -package com.instructure.student.ui - -import com.instructure.student.ui.utils.StudentTest -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test +package com.instructure.pandautils.features.assignments.details -@HiltAndroidTest -class LoginFindSchoolPageTest: StudentTest() { +import com.instructure.pandautils.features.grades.SubmissionStateLabel - // Runs live; no MockCanvas - @Test - override fun displaysPageObjects() { - loginLandingPage.clickFindMySchoolButton() - loginFindSchoolPage.assertPageObjects() - } -} +data class DiscussionCheckpointViewState( + val name: String, + val stateLabel: SubmissionStateLabel, + val grade: String, + val courseColor: Int +) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DiscussionCheckpointLayout.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DiscussionCheckpointLayout.kt new file mode 100644 index 0000000000..b4894bbfa0 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DiscussionCheckpointLayout.kt @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.pandautils.features.assignments.details.composables + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.instructure.pandautils.R +import com.instructure.pandautils.compose.composables.CanvasDivider +import com.instructure.pandautils.compose.composables.SubmissionState +import com.instructure.pandautils.features.assignments.details.DiscussionCheckpointViewState +import com.instructure.pandautils.features.grades.SubmissionStateLabel + +@Composable +fun DiscussionCheckpointLayout( + checkpoints: List, + modifier: Modifier = Modifier +) { + if (checkpoints.isEmpty()) return + + Column( + modifier = modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 8.dp) + .background( + color = colorResource(id = R.color.backgroundLightest), + shape = RoundedCornerShape(6.dp) + ) + .border( + width = 1.dp, + color = colorResource(id = R.color.borderMedium), + shape = RoundedCornerShape(6.dp) + ) + .padding(16.dp) + ) { + checkpoints.forEachIndexed { index, checkpoint -> + CheckpointItem( + checkpoint = checkpoint, + modifier = Modifier.testTag("checkpointItem-$index") + ) + if (index < checkpoints.lastIndex) { + CanvasDivider(modifier = Modifier.padding(vertical = 8.dp)) + } + } + } +} + +@Composable +private fun CheckpointItem( + checkpoint: DiscussionCheckpointViewState, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .padding(vertical = 4.dp) + .fillMaxWidth() + .semantics(true) {}, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = checkpoint.name, + fontSize = 16.sp, + color = colorResource(id = R.color.textDarkest), + modifier = Modifier.testTag("checkpointName") + ) + + SubmissionState( + submissionStateLabel = checkpoint.stateLabel, + testTag = "checkpointStatus" + ) + } + + Text( + text = checkpoint.grade, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = Color(checkpoint.courseColor), + modifier = Modifier + .padding(start = 8.dp) + .testTag("checkpointGrade") + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun DiscussionCheckpointLayoutPreview() { + DiscussionCheckpointLayout( + checkpoints = listOf( + DiscussionCheckpointViewState( + name = "Reply to topic", + stateLabel = SubmissionStateLabel.Graded, + grade = "5/5", + courseColor = android.graphics.Color.RED + ), + DiscussionCheckpointViewState( + name = "Additional replies (3)", + stateLabel = SubmissionStateLabel.Graded, + grade = "2.5/5", + courseColor = android.graphics.Color.RED + ) + ) + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DueDateReminderLayout.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DueDateReminderLayout.kt new file mode 100644 index 0000000000..8baf3541da --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/composables/DueDateReminderLayout.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.pandautils.features.assignments.details.composables + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.instructure.pandares.R +import com.instructure.pandautils.compose.composables.CanvasDivider +import com.instructure.pandautils.features.reminder.ReminderViewState +import com.instructure.pandautils.features.reminder.composables.ReminderView +import com.instructure.pandautils.utils.toFormattedString + +@Composable +fun DueDateReminderLayout( + reminderViewStates: List, + onAddClick: (String?) -> Unit, + onRemoveClick: (Long) -> Unit, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + reminderViewStates.forEachIndexed { index, reminderViewState -> + DueDateBlock(reminderViewState, index) + ReminderView( + viewState = reminderViewState, + onAddClick = onAddClick, + onRemoveClick = onRemoveClick, + modifier = Modifier.testTag("reminderView-$index") + ) + CanvasDivider() + } + } +} + +@Composable +private fun DueDateBlock( + reminderViewState: ReminderViewState, + position: Int +) { + Column(modifier = Modifier.testTag("dueDateColumn-${reminderViewState.dueLabel}")) { + Text( + modifier = Modifier + .padding(top = 24.dp, start = 16.dp, end = 16.dp) + .semantics { heading() } + .testTag("dueDateHeaderText-${reminderViewState.dueLabel}"), + text = reminderViewState.dueLabel ?: stringResource(id = R.string.dueLabel), + color = colorResource(id = R.color.textDark), + fontSize = 14.sp + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + modifier = Modifier + .padding(bottom = 14.dp, start = 16.dp, end = 16.dp) + .testTag("dueDateText-$position"), + text = reminderViewState.dueDate?.toFormattedString() + ?: stringResource(R.string.toDoNoDueDate), + color = colorResource(id = R.color.textDarkest), + fontSize = 16.sp + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListFragment.kt index 41a4baf7b8..c00b9ea02d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListFragment.kt @@ -39,12 +39,16 @@ import com.instructure.pandautils.analytics.SCREEN_VIEW_ASSIGNMENT_LIST import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.base.BaseCanvasFragment import com.instructure.pandautils.features.assignments.list.composables.AssignmentListScreen +import com.instructure.pandautils.utils.AssignmentGradedEvent import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.ViewStyler import com.instructure.pandautils.utils.collectOneOffEvents import com.instructure.pandautils.utils.color import com.instructure.pandautils.utils.withArgs import dagger.hilt.android.AndroidEntryPoint +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import javax.inject.Inject @ScreenView(SCREEN_VIEW_ASSIGNMENT_LIST) @@ -58,6 +62,16 @@ class AssignmentListFragment: BaseCanvasFragment(), Bookmarkable { override val bookmark: Bookmarker by lazy { viewModel.bookmarker } + override fun onStart() { + super.onStart() + EventBus.getDefault().register(this) + } + + override fun onStop() { + super.onStop() + EventBus.getDefault().unregister(this) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -100,6 +114,14 @@ class AssignmentListFragment: BaseCanvasFragment(), Bookmarkable { return getString(R.string.assignmentListTitle) } + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onAssignmentGraded(event: AssignmentGradedEvent) { + event.once(javaClass.simpleName) { + viewModel.handleAction(AssignmentListScreenEvent.Refresh) + } + } + companion object { fun newInstance(): AssignmentListFragment { return AssignmentListFragment() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModel.kt index 7b1ad27314..61d3826a12 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/AssignmentListViewModel.kt @@ -49,6 +49,7 @@ import com.instructure.pandautils.utils.orderedCheckpoints import com.instructure.pandautils.utils.toFormattedString import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -79,6 +80,8 @@ class AssignmentListViewModel @Inject constructor( private var customStatuses = listOf() + private var loadJob: Job? = null + init { getAssignments(false) } @@ -88,8 +91,9 @@ class AssignmentListViewModel @Inject constructor( } private fun getAssignments(forceRefresh: Boolean = false) { + loadJob?.cancel() if (courseId != null) { - viewModelScope.tryLaunch { + loadJob = viewModelScope.tryLaunch { val course = repository.getCourse(courseId, forceRefresh) customStatuses = repository.getCustomGradeStatuses(courseId, forceRefresh) bookmarker = Bookmarker(true, course) @@ -258,15 +262,29 @@ class AssignmentListViewModel @Inject constructor( filteredAssignments = assignmentFilters.flatMap { assignmentFilter -> when(assignmentFilter) { AssignmentFilter.All -> filteredAssignments - AssignmentFilter.NotYetSubmitted -> filteredAssignments.filter { !it.isSubmitted && it.isOnlineSubmissionType } // TODO: Need to filter by Checkpoints but Sub-assignments do not have a submittedAt field yet - AssignmentFilter.ToBeGraded -> filteredAssignments.filter { it.isSubmitted && !it.isGraded() && it.isOnlineSubmissionType } // TODO: Need to filter by Checkpoints but Sub-assignments do not have a submittedAt field yet - AssignmentFilter.Graded -> filteredAssignments.filter { - val hasGradedCheckpoint = it.submission?.subAssignmentSubmissions?.any { subAssignmentSubmission -> - subAssignmentSubmission.grade != null || subAssignmentSubmission.customGradeStatusId != null - }.orDefault() - it.isGraded() || hasGradedCheckpoint && it.isOnlineSubmissionType + AssignmentFilter.NotYetSubmitted -> filteredAssignments.filter { assignment -> + val parentNotSubmitted = !assignment.isSubmitted && assignment.isOnlineSubmissionType + val hasUnsubmittedCheckpoint = assignment.hasAnyCheckpointWithoutGrade() + parentNotSubmitted || hasUnsubmittedCheckpoint + } + AssignmentFilter.ToBeGraded -> filteredAssignments.filter { assignment -> + val parentToBeGraded = assignment.isSubmitted && !assignment.isGraded() && assignment.isOnlineSubmissionType + val hasCheckpointToBeGraded = assignment.hasAnyCheckpointWithoutGrade() + parentToBeGraded || (hasCheckpointToBeGraded && assignment.isOnlineSubmissionType) + } + AssignmentFilter.Graded -> filteredAssignments.filter { assignment -> + val hasGradedCheckpoint = assignment.hasAnyCheckpointWithGrade() + assignment.isGraded() || (hasGradedCheckpoint && assignment.isOnlineSubmissionType) + } + AssignmentFilter.Other -> filteredAssignments.filterNot { assignment -> + val notYetSubmitted = !assignment.isSubmitted && assignment.isOnlineSubmissionType + val toBeGraded = assignment.isSubmitted && !assignment.isGraded() && assignment.isOnlineSubmissionType + val graded = assignment.isGraded() && assignment.isOnlineSubmissionType + val hasCheckpointNotYetSubmitted = assignment.hasAnyCheckpointWithoutGrade() + val hasCheckpointGraded = assignment.hasAnyCheckpointWithGrade() + + notYetSubmitted || toBeGraded || graded || hasCheckpointNotYetSubmitted || hasCheckpointGraded } - AssignmentFilter.Other -> filteredAssignments.filterNot { (!it.isSubmitted && it.isOnlineSubmissionType) || (it.isSubmitted && !it.isGraded() && it.isOnlineSubmissionType) || (it.isGraded() && it.isOnlineSubmissionType) } AssignmentFilter.NeedsGrading -> filteredAssignments.filter { it.needsGradingCount > 0 } AssignmentFilter.NotSubmitted -> filteredAssignments.filter { it.unpublishable } } @@ -282,14 +300,38 @@ class AssignmentListViewModel @Inject constructor( selectedFilters.selectedGradingPeriodFilter?.let { gradingPeriodFilter -> if (uiState.value.gradingPeriods.isNotEmpty()) { - filteredAssignments = filteredAssignments.filter { - uiState.value.gradingPeriodsWithAssignments[gradingPeriodFilter]?.map { it.id } - ?.contains(it.id).orDefault() + filteredAssignments = filteredAssignments.filter { assignment -> + val assignmentInPeriod = uiState.value.gradingPeriodsWithAssignments[gradingPeriodFilter]?.map { it.id } + ?.contains(assignment.id).orDefault() + + // For DCP assignments, check if any checkpoint falls within the grading period + val hasCheckpointInPeriod = if (assignment.checkpoints.isNotEmpty()) { + val periodStart = gradingPeriodFilter.startDate?.toDate() + val periodEnd = gradingPeriodFilter.endDate?.toDate() + + assignment.checkpoints.any { checkpoint -> + val checkpointDueDate = checkpoint.dueDate + checkpointDueDate != null && periodStart != null && periodEnd != null && + !checkpointDueDate.before(periodStart) && !checkpointDueDate.after(periodEnd) + } + } else { + false + } + + assignmentInPeriod || hasCheckpointInPeriod } } } - filteredAssignments = filteredAssignments.sortedBy { it.id }.distinct() + filteredAssignments = filteredAssignments + .sortedWith( + compareBy( + { it.dueDateIncludingCheckpoints() == null }, + { it.dueDateIncludingCheckpoints() }, + { it.id } + ) + ) + .distinct() val groups = when(selectedFilters.selectedGroupByOption) { AssignmentGroupByOption.DueDate -> { @@ -326,12 +368,13 @@ class AssignmentListViewModel @Inject constructor( } AssignmentGroupByOption.AssignmentGroup -> { filteredAssignments.groupBy { it.assignmentGroupId }.map { (key, value) -> - uiState.value.assignmentGroups.firstOrNull { it.id == key }?.name.orEmpty() to value.map { + val group = uiState.value.assignmentGroups.firstOrNull { it.id == key } + group?.position.orDefault() to (group?.name.orEmpty() to value.map { assignmentListBehavior.getAssignmentGroupItemState( course, it, customStatuses, getCheckpoints(it, course) ) - } - }.toMap() + }) + }.sortedBy { it.first }.associate { it.second } } AssignmentGroupByOption.AssignmentType -> { val discussionsGroup = filteredAssignments.filter { @@ -410,4 +453,27 @@ class AssignmentListViewModel @Inject constructor( private fun Assignment.dueDateIncludingCheckpoints(): Date? { return (dueAt ?: orderedCheckpoints.firstOrNull { it.dueAt != null }?.dueAt).toDate() } + + private fun Assignment.hasAnyCheckpointWithGrade(): Boolean { + return if (checkpoints.isNotEmpty()) { + submission?.subAssignmentSubmissions?.any { subAssignmentSubmission -> + subAssignmentSubmission.grade != null || subAssignmentSubmission.customGradeStatusId != null + }.orDefault() + } else { + false + } + } + + private fun Assignment.hasAnyCheckpointWithoutGrade(): Boolean { + return if (checkpoints.isNotEmpty()) { + submission?.subAssignmentSubmissions?.let { submissions -> + checkpoints.any { checkpoint -> + val checkpointSubmission = submissions.find { it.subAssignmentTag == checkpoint.tag } + checkpointSubmission?.grade == null && checkpointSubmission?.customGradeStatusId == null + } + } ?: true // If no submissions exist, all checkpoints are unsubmitted + } else { + false + } + } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/composables/AssignmentListScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/composables/AssignmentListScreen.kt index 7db8edc714..56cf0dd4a6 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/composables/AssignmentListScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/list/composables/AssignmentListScreen.kt @@ -596,7 +596,7 @@ private fun AssignmentListItemView( toggleCheckpointsExpanded(item.assignment.id) } .semantics { - testTag = "expandDiscussionCheckpoint" + testTag = "expandDiscussionCheckpoints" role = Role.Button } ) { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/details/EventViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/details/EventViewModel.kt index 2086439cfe..fb1e5dcbb7 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/details/EventViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/details/EventViewModel.kt @@ -150,7 +150,7 @@ class EventViewModel @Inject constructor( recurrence = scheduleItem.seriesNaturalLanguage.orEmpty(), location = scheduleItem.locationName.orEmpty(), address = scheduleItem.locationAddress.orEmpty(), - formattedDescription = htmlContentFormatter.formatHtmlWithIframes(scheduleItem.description.orEmpty()), + formattedDescription = htmlContentFormatter.formatHtmlWithIframes(scheduleItem.description.orEmpty(), scheduleItem.courseId), isSeriesEvent = scheduleItem.isRecurring, isSeriesHead = scheduleItem.seriesHead, isMessageFabEnabled = eventViewModelBehavior.shouldShowMessageFab && scheduleItem.contextType == CanvasContext.Type.COURSE diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt index 028f55bfbb..de52b82926 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt @@ -24,7 +24,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.work.WorkInfo import androidx.work.WorkManager -import androidx.work.await import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.managers.AccountNotificationManager import com.instructure.canvasapi2.managers.ConferenceManager @@ -64,6 +63,7 @@ import com.instructure.pandautils.room.offline.daos.StudioMediaProgressDao import com.instructure.pandautils.utils.orDefault import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.threeten.bp.OffsetDateTime import java.util.Locale @@ -292,36 +292,33 @@ class DashboardNotificationsViewModel @Inject constructor( private suspend fun getUploads(fileUploadEntities: List?) = fileUploadEntities?.mapNotNull { fileUploadEntity -> val workerId = UUID.fromString(fileUploadEntity.workerId) - workManager.getWorkInfoById(workerId).await()?.let { workInfo -> + val workInfo = workManager.getWorkInfoByIdFlow(workerId).first() + workInfo?.let { val icon: Int val background: Int - when (workInfo.state) { + when (it.state) { WorkInfo.State.FAILED -> { icon = R.drawable.ic_exclamation_mark background = R.color.backgroundDanger } - WorkInfo.State.SUCCEEDED -> { icon = R.drawable.ic_check_white_24dp background = R.color.backgroundSuccess } - else -> { icon = R.drawable.ic_upload background = R.color.backgroundInfo } } - val uploadViewData = UploadViewData( fileUploadEntity.title.orEmpty(), fileUploadEntity.subtitle.orEmpty(), - icon, background, workInfo.state == WorkInfo.State.RUNNING + icon, background, it.state == WorkInfo.State.RUNNING ) - UploadItemViewModel( workerId = workerId, workManager = workManager, data = uploadViewData, - open = { uuid -> openUploadNotification(workInfo.state, uuid, fileUploadEntity) }, + open = { uuid -> openUploadNotification(it.state, uuid, fileUploadEntity) }, remove = { removeUploadNotification(fileUploadEntity, workerId) } ) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/UploadItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/UploadItemViewModel.kt index f0d2e65c20..48d45185c7 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/UploadItemViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/UploadItemViewModel.kt @@ -19,8 +19,6 @@ package com.instructure.pandautils.features.dashboard.notifications.itemviewmode import androidx.databinding.BaseObservable import androidx.databinding.Bindable -import androidx.lifecycle.Observer -import androidx.work.WorkInfo import androidx.work.WorkManager import com.instructure.pandautils.BR import com.instructure.pandautils.R @@ -28,7 +26,12 @@ import com.instructure.pandautils.features.dashboard.notifications.UploadViewDat import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker.Companion.PROGRESS_DATA_FULL_SIZE import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker.Companion.PROGRESS_DATA_UPLOADED_SIZE import com.instructure.pandautils.mvvm.ItemViewModel -import java.util.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import java.util.UUID class UploadItemViewModel( val workerId: UUID, @@ -39,23 +42,25 @@ class UploadItemViewModel( val remove: () -> Unit, @get:Bindable var loading: Boolean = false ) : ItemViewModel, BaseObservable() { - override val layoutId = R.layout.item_dashboard_upload - - private val observer = Observer { - val uploadedSize = it.progress.getLong(PROGRESS_DATA_UPLOADED_SIZE, 0L) - val fullSize = it.progress.getLong(PROGRESS_DATA_FULL_SIZE, 1L) - - progress = ((uploadedSize.toDouble() / fullSize.toDouble()) * 100.0).toInt() - notifyPropertyChanged(BR.progress) - } + private var job: Job? = null init { - workManager.getWorkInfoByIdLiveData(workerId).observeForever(observer) + job = CoroutineScope(Dispatchers.Main).launch { + workManager.getWorkInfoByIdFlow(workerId).collectLatest { workInfo -> + workInfo?.let { + val uploadedSize = it.progress.getLong(PROGRESS_DATA_UPLOADED_SIZE, 0L) + val fullSize = it.progress.getLong(PROGRESS_DATA_FULL_SIZE, 1L) + + progress = ((uploadedSize.toDouble() / fullSize.toDouble()) * 100.0).toInt() + notifyPropertyChanged(BR.progress) + } + } + } } fun clear() { - workManager.getWorkInfoByIdLiveData(workerId).removeObserver(observer) + job?.cancel() } fun open() = open.invoke(workerId) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouter.kt index 5ed1e84a89..41bc97c2a2 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouter.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouter.kt @@ -10,5 +10,7 @@ interface DiscussionRouter { fun routeToGroupDiscussion(group: Group, id: Long, header: DiscussionTopicHeader, isRedesign: Boolean) + fun routeToDiscussionWebView(canvasContext: CanvasContext, discussionTopicHeaderId: Long) + fun routeToNativeSpeedGrader(courseId: Long, assignmentId: Long, submissionIds: List, selectedIdx: Int, anonymousGrading: Boolean?, discussionTopicEntryId: Long?) } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterFragment.kt index 4cdbbb4f02..62e891077e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterFragment.kt @@ -68,6 +68,9 @@ class DiscussionRouterFragment : BaseCanvasFragment() { is DiscussionRouterAction.RouteToGroupDiscussion -> { discussionRouter.routeToGroupDiscussion(action.group, action.id, action.header, action.isRedesignEnabled) } + is DiscussionRouterAction.RouteToDiscussionWebView -> { + discussionRouter.routeToDiscussionWebView(action.canvasContext, action.discussionTopicHeaderId) + } is DiscussionRouterAction.ShowToast -> { toast(action.toast, Toast.LENGTH_SHORT) Handler(Looper.getMainLooper()).post { requireActivity().onBackPressed() } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewData.kt index ba77273da1..54127dff72 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewData.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewData.kt @@ -19,5 +19,10 @@ sealed class DiscussionRouterAction { val isRedesignEnabled: Boolean ) : DiscussionRouterAction() + data class RouteToDiscussionWebView( + val canvasContext: CanvasContext, + val discussionTopicHeaderId: Long + ) : DiscussionRouterAction() + data class ShowToast(val toast: String) : DiscussionRouterAction() } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModel.kt index 861c58b901..688200f729 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModel.kt @@ -36,18 +36,34 @@ class DiscussionRouterViewModel @Inject constructor( val header = discussionTopicHeader ?: discussionRouteHelper.getDiscussionHeader( canvasContext, discussionTopicHeaderId - )!! + ) + + if (header == null) { + // Unable to fetch discussion header (e.g., 404 for anonymous discussions) + if (shouldShowDiscussionRedesign) { + // Fallback to web view for redesigned discussions + routeToDiscussionWebView(canvasContext, discussionTopicHeaderId, isAnnouncement) + } else { + // Show error for non-redesigned discussions + _events.postValue(Event(DiscussionRouterAction.ShowToast(resources.getString(R.string.discussionErrorToast)))) + } + return@launch + } if (header.groupTopicChildren.isNotEmpty()) { val discussionGroup = discussionRouteHelper.getDiscussionGroup(header) discussionGroup?.let { - val groupDiscussionHeader = discussionRouteHelper.getDiscussionHeader(it.first, it.second)!! - routeToDiscussionGroup( - it.first, - it.second, - groupDiscussionHeader, - shouldShowDiscussionRedesign - ) + val groupDiscussionHeader = discussionRouteHelper.getDiscussionHeader(it.first, it.second) + if (groupDiscussionHeader != null) { + routeToDiscussionGroup( + it.first, + it.second, + groupDiscussionHeader, + shouldShowDiscussionRedesign + ) + } else { + routeToDiscussion(canvasContext, header, shouldShowDiscussionRedesign, isAnnouncement) + } } ?: routeToDiscussion(canvasContext, header, shouldShowDiscussionRedesign, isAnnouncement) } else { routeToDiscussion(canvasContext, header, shouldShowDiscussionRedesign, isAnnouncement) @@ -95,4 +111,19 @@ class DiscussionRouterViewModel @Inject constructor( ) ) } + + private fun routeToDiscussionWebView( + canvasContext: CanvasContext, + discussionTopicHeaderId: Long, + isAnnouncement: Boolean + ) { + _events.postValue( + Event( + DiscussionRouterAction.RouteToDiscussionWebView( + canvasContext, + discussionTopicHeaderId + ) + ) + ) + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomViewModel.kt index 8ae1d781cc..9a16c27994 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomViewModel.kt @@ -169,8 +169,8 @@ class HomeroomViewModel @Inject constructor( ): AnnouncementItemViewModel? { return if (announcement != null) { val htmlWithIframes = htmlContentFormatter.formatHtmlWithIframes( - announcement.message - ?: "" + announcement.message ?: "", + course.id ) AnnouncementItemViewModel( AnnouncementViewData(course.name, announcement.title ?: "", htmlWithIframes), diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt index 35af8e5318..82072ba28a 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt @@ -248,7 +248,7 @@ class FileUploadDialogFragment : BaseCanvasDialogFragment() { } is FileUploadAction.UploadStartedAction -> { getParent()?.selectedUriStringsCallback(action.selectedUris) - getParent()?.workInfoLiveDataCallback(action.id, action.liveData) + getParent()?.workInfoLiveDataCallback(action.id,action.liveData) lifecycleScope.launch { fileUploadEventHandler.postEvent( FileUploadEvent.FileSelected(action.selectedUris) @@ -256,7 +256,8 @@ class FileUploadDialogFragment : BaseCanvasDialogFragment() { fileUploadEventHandler.postEvent( FileUploadEvent.UploadStarted( action.id, - action.liveData + action.liveData, + action.selectedUris ) ) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogParent.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogParent.kt index 11d4099ef7..4a16d76981 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogParent.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogParent.kt @@ -28,5 +28,5 @@ interface FileUploadDialogParent { fun selectedUriStringsCallback(filePaths: List) = Unit - fun workInfoLiveDataCallback(uuid: UUID? = null, workInfoLiveData: LiveData) = Unit + fun workInfoLiveDataCallback(uuid: UUID? = null, workInfoLiveData: LiveData) = Unit } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewData.kt index bc47a0eaf8..185b8efb7e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewData.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewData.kt @@ -42,6 +42,6 @@ sealed class FileUploadAction { object UploadStarted : FileUploadAction() data class ShowToast(val toast: String) : FileUploadAction() data class AttachmentSelectedAction(val event: Int, val attachment: FileSubmitObject?) : FileUploadAction() - data class UploadStartedAction(val id: UUID, val liveData: LiveData, val selectedUris: List) : FileUploadAction() + data class UploadStartedAction(val id: UUID, val liveData: LiveData, val selectedUris: List) : FileUploadAction() } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt index d2eda653f3..5819dfac76 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt @@ -38,7 +38,7 @@ import com.instructure.pandautils.utils.orDefault import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.io.File -import java.util.* +import java.util.UUID import javax.inject.Inject @HiltViewModel @@ -292,7 +292,9 @@ class FileUploadDialogViewModel @Inject constructor( if (uploadType == FileUploadType.DISCUSSION) { _events.value = Event(FileUploadAction.AttachmentSelectedAction(FileUploadDialogFragment.EVENT_ON_FILE_SELECTED, getAttachmentUri())) } else { - val worker = OneTimeWorkRequestBuilder().build() + val worker = OneTimeWorkRequestBuilder() + .addTag("FileUploadWorker") + .build() val input = getInputData(worker.id, uris) fileUploadInputDao.insert(input) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadEventHandler.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadEventHandler.kt index 5c18ce2721..9510bbc275 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadEventHandler.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadEventHandler.kt @@ -29,7 +29,8 @@ sealed class FileUploadEvent { data class FileSelected(val filePaths: List) : FileUploadEvent() data class UploadStarted( val uuid: UUID?, - val workInfoLiveData: LiveData + val workInfoLiveData: LiveData, + val filePaths: List ) : FileUploadEvent() } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt index 1d6f2c845d..0a7d5accce 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt @@ -331,7 +331,7 @@ class FileUploadWorker @AssistedInject constructor( } }).dataOrThrow - val updatedList = workDataBuilder.build() + val updatedList: Array = workDataBuilder.build() .getStringArray(PROGRESS_DATA_UPLOADED_FILES) .orEmpty() .toMutableList() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesScreen.kt index c4ab26e01f..0920811491 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesScreen.kt @@ -82,7 +82,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.invisibleToUser +import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag @@ -291,7 +291,7 @@ private fun GradesScreenContent( modifier = Modifier .height(24.dp) .semantics { - invisibleToUser() + hideFromAccessibility() } ) } @@ -301,19 +301,19 @@ private fun GradesScreenContent( } } - uiState.items.forEach { + uiState.items.forEach { item -> stickyHeader { GroupHeader( - name = it.name, - expanded = it.expanded, + name = item.name, + expanded = item.expanded, onClick = { - actionHandler(GradesAction.GroupHeaderClick(it.id)) + actionHandler(GradesAction.GroupHeaderClick(item.id)) } ) } - if (it.expanded) { - items(it.assignments) { assignment -> + if (item.expanded) { + items(item.assignments) { assignment -> AssignmentItem(assignment, actionHandler, userColor) } } @@ -548,7 +548,7 @@ fun AssignmentItem( actionHandler(GradesAction.ToggleCheckpointsExpanded(uiState.id)) } .semantics { - testTag = "expandDiscussionCheckpoint" + testTag = "expandDiscussionCheckpoints" role = Role.Button } ) { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeFragment.kt index 9f8d3124c7..c8097976b8 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeFragment.kt @@ -107,9 +107,15 @@ class InboxComposeFragment : BaseCanvasFragment(), FragmentInteractions, FileUpl return this } - override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { + override fun selectedUriStringsCallback(filePaths: List) { + viewModel.addUploadingAttachments(filePaths) + } + + override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) { workInfoLiveData.observe(viewLifecycleOwner) { workInfo -> - viewModel.updateAttachments(uuid, workInfo) + workInfo?.let { + viewModel.updateAttachments(uuid, workInfo) + } } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModel.kt index 9ca5cc44da..028ede9212 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/compose/InboxComposeViewModel.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.work.WorkInfo import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Recipient import com.instructure.canvasapi2.type.EnrollmentType import com.instructure.canvasapi2.utils.DataResult @@ -47,6 +48,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.util.Date import java.util.EnumMap import java.util.UUID import javax.inject.Inject @@ -208,19 +210,110 @@ class InboxComposeViewModel @Inject constructor( } } - fun updateAttachments(uuid: UUID?, workInfo: WorkInfo) { - if (workInfo.state == WorkInfo.State.SUCCEEDED) { - viewModelScope.launch { - uuid?.let { uuid -> - val attachmentEntities = attachmentDao.findByParentId(uuid.toString()) - val status = workInfo.state.toAttachmentCardStatus() - attachmentEntities?.let { attachmentList -> - _uiState.update { it.copy(attachments = it.attachments + attachmentList.map { AttachmentCardItem(it.toApiModel(), status, false) }) } - attachmentDao.deleteAll(attachmentList) - } ?: sendScreenResult(context.getString(R.string.errorUploadingFile)) - } ?: sendScreenResult(context.getString(R.string.errorUploadingFile)) + fun addUploadingAttachments(filePaths: List) { + // Create placeholder attachments with UPLOADING status from file URIs + val placeholderAttachments = filePaths.mapIndexed { index, path -> + val fileName = path.substringAfterLast("/") + val attachment = com.instructure.canvasapi2.models.Attachment( + id = System.currentTimeMillis() + index, // Temporary ID until real one is assigned + filename = fileName, + displayName = fileName, + contentType = "" + ) + AttachmentCardItem(attachment, AttachmentStatus.UPLOADING, false) + } - } + _uiState.update { it.copy(attachments = it.attachments + placeholderAttachments) } + } + + fun updateAttachments(uuid: UUID?, workInfo: WorkInfo) { + viewModelScope.launch { + uuid?.let { workerId -> + val status = workInfo.state.toAttachmentCardStatus() + + when (workInfo.state) { + WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING -> { + // Update progress for uploading attachments + val progress = workInfo.progress + val uploadedSize = progress.getLong("PROGRESS_DATA_UPLOADED_SIZE", 0L) + val totalSize = progress.getLong("PROGRESS_DATA_TOTAL_SIZE", 0L) + val progressPercent = if (totalSize > 0) uploadedSize.toFloat() / totalSize.toFloat() else 0f + + // Find attachment with this workerId, or assign workerId to first placeholder without one + _uiState.update { currentState -> + var workerIdAssigned = false + currentState.copy( + attachments = currentState.attachments.map { attachment -> + when { + // Update attachment that already has this workerId + attachment.workerId == workerId.toString() -> { + attachment.copy(uploadProgress = progressPercent) + } + // Assign workerId to first placeholder without one + !workerIdAssigned && attachment.status == AttachmentStatus.UPLOADING && attachment.workerId == null -> { + workerIdAssigned = true + attachment.copy(workerId = workerId.toString(), uploadProgress = progressPercent) + } + else -> attachment + } + } + ) + } + } + WorkInfo.State.SUCCEEDED -> { + // Replace placeholder attachment with matching workerId with real uploaded attachments + val attachmentEntities = attachmentDao.findByParentId(workerId.toString()) + attachmentEntities?.let { attachmentList -> + // Create real uploaded attachments + val uploadedAttachments = attachmentList.map { + AttachmentCardItem(it.toApiModel(), status, false) + } + + // Remove placeholder with matching workerId, or first UPLOADING placeholder without workerId + _uiState.update { currentState -> + var placeholderRemoved = false + val filteredAttachments = currentState.attachments.filter { attachment -> + when { + attachment.workerId == workerId.toString() -> { + placeholderRemoved = true + false // Remove this placeholder + } + !placeholderRemoved && attachment.status == AttachmentStatus.UPLOADING && attachment.workerId == null -> { + placeholderRemoved = true + false // Remove first unassigned placeholder + } + else -> true + } + } + currentState.copy(attachments = filteredAttachments + uploadedAttachments) + } + attachmentDao.deleteAll(attachmentList) + } ?: sendScreenResult(context.getString(R.string.errorUploadingFile)) + } + WorkInfo.State.FAILED, WorkInfo.State.CANCELLED, WorkInfo.State.BLOCKED -> { + // Update placeholder with matching workerId to FAILED, or first UPLOADING placeholder without workerId + _uiState.update { currentState -> + var placeholderUpdated = false + currentState.copy( + attachments = currentState.attachments.map { attachment -> + when { + attachment.workerId == workerId.toString() -> { + placeholderUpdated = true + attachment.copy(status = AttachmentStatus.FAILED) + } + !placeholderUpdated && attachment.status == AttachmentStatus.UPLOADING && attachment.workerId == null -> { + placeholderUpdated = true + attachment.copy(status = AttachmentStatus.FAILED, workerId = workerId.toString()) + } + else -> attachment + } + } + ) + } + sendScreenResult(context.getString(R.string.errorUploadingFile)) + } + } + } ?: sendScreenResult(context.getString(R.string.errorUploadingFile)) } } @@ -532,12 +625,31 @@ class InboxComposeViewModel @Inject constructor( return inboxComposeRepository.getRecipients(searchQuery, contextId, forceRefresh) } + private fun canSendMessageInContext(canvasContext: CanvasContext): Boolean { + if (canvasContext !is Course) return true + + val fullCourse = uiState.value.selectContextUiState.canvasContexts + .filterIsInstance() + .find { it.id == canvasContext.id } + ?: canvasContext + + val now = Date() + return fullCourse.workflowState != Course.WorkflowState.COMPLETED && + fullCourse.endDate?.before(now) != true && + fullCourse.term?.endDate?.before(now) != true + } + private fun createConversation() { uiState.value.selectContextUiState.selectedCanvasContext?.let { canvasContext -> viewModelScope.launch { _uiState.update { uiState.value.copy(screenState = ScreenState.Loading) } try { + if (!canSendMessageInContext(canvasContext)) { + sendScreenResult(context.getString(R.string.courseConcludedError)) + return@launch + } + inboxComposeRepository.createConversation( recipients = uiState.value.recipientPickerUiState.selectedRecipients, subject = uiState.value.subject.text, @@ -569,6 +681,11 @@ class InboxComposeViewModel @Inject constructor( _uiState.update { uiState.value.copy(screenState = ScreenState.Loading) } try { + if (!canSendMessageInContext(canvasContext)) { + sendScreenResult(context.getString(R.string.courseConcludedError)) + return@launch + } + inboxComposeRepository.addMessage( conversationId = uiState.value.previousMessages?.conversation?.id ?: 0, recipients = uiState.value.recipientPickerUiState.selectedRecipients, diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCard.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCard.kt index 9ad4391dac..0be744708c 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCard.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCard.kt @@ -130,7 +130,18 @@ fun AttachmentCard( if (!attachmentCardItem.readOnly){ when (status) { AttachmentStatus.UPLOADING -> { - Loading() + val progress = attachmentCardItem.uploadProgress + if (progress != null && progress > 0f) { + // Show progress percentage + Text( + text = "${(progress * 100).toInt()}%", + color = colorResource(id = R.color.textDark), + fontSize = 14.sp + ) + } else { + // Show indeterminate loading spinner + Loading() + } } AttachmentStatus.UPLOADED -> { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCardItem.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCardItem.kt index a52a409c13..ce2e4ffd6f 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCardItem.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/utils/AttachmentCardItem.kt @@ -21,7 +21,9 @@ import com.instructure.canvasapi2.models.Attachment data class AttachmentCardItem ( val attachment: Attachment, val status: AttachmentStatus, // TODO: Currently this is not used for proper state handling, but if the upload process will be refactored it can be useful - val readOnly: Boolean + val readOnly: Boolean, + val uploadProgress: Float? = null, // Upload progress from 0.0 to 1.0, null if not uploading + val workerId: String? = null // WorkManager UUID to track which upload this belongs to ) enum class AttachmentStatus { @@ -34,12 +36,12 @@ enum class AttachmentStatus { companion object { fun fromWorkInfoState(state: WorkInfo.State): AttachmentStatus { return when (state) { - WorkInfo.State.SUCCEEDED -> AttachmentStatus.UPLOADED - WorkInfo.State.FAILED -> AttachmentStatus.FAILED - WorkInfo.State.ENQUEUED -> AttachmentStatus.UPLOADING - WorkInfo.State.RUNNING -> AttachmentStatus.UPLOADING - WorkInfo.State.BLOCKED -> AttachmentStatus.FAILED - WorkInfo.State.CANCELLED -> AttachmentStatus.FAILED + WorkInfo.State.SUCCEEDED -> UPLOADED + WorkInfo.State.FAILED -> FAILED + WorkInfo.State.ENQUEUED -> UPLOADING + WorkInfo.State.RUNNING -> UPLOADING + WorkInfo.State.BLOCKED -> FAILED + WorkInfo.State.CANCELLED -> FAILED } } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/lti/LtiLaunchRepository.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/lti/LtiLaunchRepository.kt index 19600623dd..735f174381 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/lti/LtiLaunchRepository.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/lti/LtiLaunchRepository.kt @@ -30,7 +30,8 @@ class LtiLaunchRepository( suspend fun getLtiFromAuthenticationUrl(url: String, ltiTool: LTITool?): LTITool { val params = RestParams(isForceReadFromNetwork = true) return ltiTool?.let { - assignmentApi.getExternalToolLaunchUrl(ltiTool.courseId, ltiTool.id, ltiTool.assignmentId, restParams = params).dataOrNull + val courseId = if (ltiTool.courseId == 0L) ltiTool.contextId ?: 0L else ltiTool.courseId + assignmentApi.getExternalToolLaunchUrl(courseId, ltiTool.id, ltiTool.assignmentId, restParams = params).dataOrNull } ?: launchDefinitionsApi.getLtiFromAuthenticationUrl(url, RestParams(isForceReadFromNetwork = true)).dataOrThrow } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt index 256d0d0a07..b7a1f839a2 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt @@ -31,10 +31,12 @@ import com.instructure.canvasapi2.apis.FileFolderAPI import com.instructure.canvasapi2.apis.GroupAPI import com.instructure.canvasapi2.apis.ModuleAPI import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.canvasapi2.apis.PlannerAPI import com.instructure.canvasapi2.apis.QuizAPI import com.instructure.canvasapi2.apis.UserAPI import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.managers.graphql.CustomGradeStatusesManager +import com.instructure.canvasapi2.managers.graphql.ModuleManager import com.instructure.canvasapi2.models.AssignmentGroup import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Conference @@ -49,19 +51,24 @@ import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.depaginate +import com.instructure.canvasapi2.utils.toApiString import com.instructure.pandautils.features.offline.offlinecontent.CourseFileSharedRepository +import com.instructure.pandautils.room.offline.daos.CheckpointDao import com.instructure.pandautils.room.offline.daos.CourseFeaturesDao import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao import com.instructure.pandautils.room.offline.daos.CustomGradeStatusDao import com.instructure.pandautils.room.offline.daos.FileFolderDao import com.instructure.pandautils.room.offline.daos.PageDao +import com.instructure.pandautils.room.offline.daos.PlannerItemDao import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.room.offline.entities.CheckpointEntity import com.instructure.pandautils.room.offline.entities.CourseFeaturesEntity import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity import com.instructure.pandautils.room.offline.entities.CustomGradeStatusEntity import com.instructure.pandautils.room.offline.entities.FileFolderEntity +import com.instructure.pandautils.room.offline.entities.PlannerItemEntity import com.instructure.pandautils.room.offline.entities.QuizEntity import com.instructure.pandautils.room.offline.facade.AssignmentFacade import com.instructure.pandautils.room.offline.facade.ConferenceFacade @@ -84,6 +91,7 @@ class CourseSync( private val userApi: UserAPI.UsersInterface, private val assignmentApi: AssignmentAPI.AssignmentInterface, private val calendarEventApi: CalendarEventAPI.CalendarEventInterface, + private val plannerApi: PlannerAPI.PlannerInterface, private val courseSyncSettingsDao: CourseSyncSettingsDao, private val pageFacade: PageFacade, private val userFacade: UserFacade, @@ -114,7 +122,10 @@ class CourseSync( private val firebaseCrashlytics: FirebaseCrashlytics, private val fileSync: FileSync, private val customGradeStatusDao: CustomGradeStatusDao, - private val customGradeStatusesManager: CustomGradeStatusesManager + private val customGradeStatusesManager: CustomGradeStatusesManager, + private val plannerItemDao: PlannerItemDao, + private val checkpointDao: CheckpointDao, + private val moduleManager: ModuleManager ) { private val additionalFileIdsToSync = mutableMapOf>() @@ -238,16 +249,37 @@ class CourseSync( private suspend fun fetchSyllabus(courseId: Long) { fetchTab(courseId, Tab.SYLLABUS_ID) { val calendarEvents = fetchCalendarEvents(courseId) - val assignmentEvents = fetchCalendarAssignments(courseId) + val assignmentEvents = fetchCalendarAssignments(courseId, CalendarEventAPI.CalendarEventType.ASSIGNMENT) + val subAssignmentEvents = fetchCalendarAssignments(courseId, CalendarEventAPI.CalendarEventType.SUB_ASSIGNMENT) val scheduleItems = mutableListOf() scheduleItems.addAll(calendarEvents) scheduleItems.addAll(assignmentEvents) + scheduleItems.addAll(subAssignmentEvents) scheduleItemFacade.insertScheduleItems(scheduleItems, courseId) + + val plannerItems = fetchPlannerItems(courseId) + plannerItemDao.deleteAllByCourseId(courseId) + plannerItemDao.insertAll(plannerItems) } } + private suspend fun fetchPlannerItems(courseId: Long): List { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true, shouldLoginOnTokenError = false) + val plannerItems = plannerApi.getPlannerItems( + null, + null, + listOf("course_$courseId"), + "new_activity", + restParams + ).depaginate { + plannerApi.nextPagePlannerItems(it, restParams) + }.dataOrThrow + + return plannerItems.map { PlannerItemEntity(it, courseId) } + } + private suspend fun fetchCalendarEvents(courseId: Long): List { val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true, shouldLoginOnTokenError = false) val calendarEvents = calendarEventApi.getCalendarEvents( @@ -266,11 +298,11 @@ class CourseSync( return calendarEvents } - private suspend fun fetchCalendarAssignments(courseId: Long): List { + private suspend fun fetchCalendarAssignments(courseId: Long, type: CalendarEventAPI.CalendarEventType): List { val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true, shouldLoginOnTokenError = false) val calendarAssignments = calendarEventApi.getCalendarEvents( true, - CalendarEventAPI.CalendarEventType.ASSIGNMENT.apiName, + type.apiName, null, null, listOf("course_$courseId"), @@ -543,6 +575,33 @@ class CourseSync( ModuleItem.Type.Quiz.name -> fetchQuizModuleItem(courseId, it, params) } } + + fetchModuleItemCheckpoints(courseId) + } + } + + private suspend fun fetchModuleItemCheckpoints(courseId: Long) { + try { + val checkpointsWithModuleItems = moduleManager.getModuleItemCheckpoints(courseId.toString(), true) + val checkpointEntities = checkpointsWithModuleItems.flatMap { moduleItemWithCheckpoints -> + moduleItemWithCheckpoints.checkpoints.map { checkpoint -> + CheckpointEntity( + assignmentId = null, + name = null, + tag = checkpoint.tag, + pointsPossible = checkpoint.pointsPossible, + dueAt = checkpoint.dueAt?.toApiString(), + onlyVisibleToOverrides = false, + lockAt = null, + unlockAt = null, + moduleItemId = moduleItemWithCheckpoints.moduleItemId.toLongOrNull(), + courseId = courseId + ) + } + } + checkpointDao.insertAll(checkpointEntities) + } catch (e: Exception) { + firebaseCrashlytics.recordException(e) } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/HtmlParser.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/HtmlParser.kt index 265ebf0259..21ee877f56 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/HtmlParser.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/HtmlParser.kt @@ -41,6 +41,7 @@ class HtmlParser( private val fileLinkRegex = Regex("]*class=\"instructure_file_link[^>]*href=\"([^\"]*)\"[^>]*>") private val internalFileRegex = Regex(".*${apiPrefs.domain}.*files/(\\d+)") private val studioIframeRegex = Regex("]*custom_arc_media_id%3D([^%]*)%[^>]*>[^<]*") + private val studioIframeExternalToolsRegex = Regex("]*src=\"[^\"]*custom_arc_media_id%3D([^%&\"]+)[^\"]*\"[^>]*>.*?", RegexOption.DOT_MATCHES_ALL) private val videoTagReplacement = """