diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..e7e6bce Binary files /dev/null and b/.DS_Store differ diff --git a/.github/actions/send-course-notification/action.yml b/.github/actions/send-course-notification/action.yml new file mode 100644 index 0000000..9652e16 --- /dev/null +++ b/.github/actions/send-course-notification/action.yml @@ -0,0 +1,64 @@ +name: Send Course Notification +description: Create fallback output if needed and send course notification to Discord + +inputs: + notification-type: + description: "Type of notification: 'canvas' or 'docker'" + required: true + output-type: + description: "Output type for fallback: 'MDXCanvas' or 'Docker'" + required: true + payload-path: + description: "Path to the output JSON file" + required: true + stdout-log: + description: "Path to stdout log file" + required: true + stderr-log: + description: "Path to stderr log file" + required: true + course-id: + description: "Course ID" + required: true + course-name: + description: "Course name" + required: true + course-url: + description: "Course URL" + required: true + discord-role: + description: "Discord CI/CD role ID" + required: true + discord-webhook-url: + description: "Discord webhook URL" + required: true + +runs: + using: composite + steps: + - name: Create fallback output if needed + shell: bash + run: | + PYTHONPATH=utils python -m notifications.fallback \ + --output-type "${{ inputs.output-type }}" \ + --output-path "${{ inputs.payload-path }}" \ + --stdout-log "${{ inputs.stdout-log }}" \ + --stderr-log "${{ inputs.stderr-log }}" \ + --action-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + - name: Send course notification to Discord + shell: bash + env: + DISCORD_WEBHOOK_URL: ${{ inputs.discord-webhook-url }} + run: | + PYTHONPATH=utils python -m notifications.send_course \ + --type "${{ inputs.notification-type }}" \ + --payload "${{ inputs.payload-path }}" \ + --course-id "${{ inputs.course-id }}" \ + --course-name "${{ inputs.course-name }}" \ + --course-url "${{ inputs.course-url }}" \ + --author "${{ github.actor }}" \ + --author-icon "https://github.com/${{ github.actor }}.png" \ + --branch "${{ github.ref_name }}" \ + --cicd-id "${{ inputs.discord-role }}" \ + --action-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/.github/workflows/docker_automation.yaml b/.github/workflows/docker_automation.yaml index 0a57452..07add43 100644 --- a/.github/workflows/docker_automation.yaml +++ b/.github/workflows/docker_automation.yaml @@ -6,6 +6,16 @@ on: course_id: required: true type: string + course_name: + required: true + type: string + course_url: + required: true + type: string + utils_ref: + required: false + default: main + type: string secrets: discord_role: required: true @@ -35,8 +45,13 @@ jobs: uses: actions/checkout@v4 with: repository: BYU-CS-Course-Ops/utils + ref: ${{ inputs.utils_ref }} path: utils + - name: Install notification dependencies + shell: bash + run: pip install discord-webhook markdowndata ${{ inputs.extra-packages }} + - name: Get changed files run: | files=$(git diff --name-only ${{ github.event.before }} ${{ github.event.after }}) @@ -72,35 +87,17 @@ jobs: --root-dir "${{ github.workspace }}" \ > "$STDOUT_LOG" 2> "$STDERR_LOG" - - name: Create fallback output if needed + - name: Send course notification if: always() - run: | - python utils/course_updates/create_fallback.py \ - --output-type Docker \ - --output-path "${{ github.workspace }}/.github/logs/docker_output.json" \ - --stdout-log "${{ github.workspace }}/.github/logs/docker_stdout.log" \ - --stderr-log "${{ github.workspace }}/.github/logs/docker_stderr.log" \ - --action-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - - - name: Get user avatar - run: | - AVATAR_URL=$(curl -s https://api.github.com/users/${{ github.actor }} | jq -r '.avatar_url') - echo "AVATAR_URL=$AVATAR_URL" >> $GITHUB_ENV - - - name: Install discord_webhook - run: | - pip install discord-webhook - - - name: Send Discord Notification - env: - DISCORD_WEBHOOK_URL: ${{ secrets.discord_webhook_url }} - run: | - python utils/course_updates/send_course_notification.py \ - --type "docker" \ - --payload "${{ github.workspace }}/.github/logs/docker_output.json" \ - --course-id "${{ inputs.course_id }}" \ - --author "${{ github.actor }}" \ - --author-icon "$AVATAR_URL" \ - --branch "${{ github.ref }}" \ - --cicd-id "${{ secrets.discord_role }}" \ - --action-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ No newline at end of file + uses: ./utils/.github/actions/send-course-notification + with: + notification-type: docker + output-type: Docker + payload-path: ${{ github.workspace }}/.github/logs/docker_output.json + stdout-log: ${{ github.workspace }}/.github/logs/docker_stdout.log + stderr-log: ${{ github.workspace }}/.github/logs/docker_stderr.log + course-id: ${{ inputs.course_id }} + course-name: ${{ inputs.course_name }} + course-url: ${{ inputs.course_url }} + discord-role: ${{ secrets.discord_role }} + discord-webhook-url: ${{ secrets.discord_webhook_url }} diff --git a/.github/workflows/mdxcanvas_automation.yaml b/.github/workflows/mdxcanvas_automation.yaml index f354e2a..99b21b3 100644 --- a/.github/workflows/mdxcanvas_automation.yaml +++ b/.github/workflows/mdxcanvas_automation.yaml @@ -6,6 +6,16 @@ on: course_id: required: true type: string + course_name: + required: true + type: string + course_url: + required: true + type: string + utils_ref: + required: false + default: main + type: string mdxcanvas_version: required: true type: string @@ -43,7 +53,7 @@ jobs: OUTPUT_PATH: ${{ github.workspace }}/.github/logs/mdxcanvas_output.json steps: - - name: Checkout repos + - name: Checkout repo uses: actions/checkout@v4 with: fetch-depth: 0 @@ -53,12 +63,18 @@ jobs: uses: actions/checkout@v4 with: repository: BYU-CS-Course-Ops/utils + ref: ${{ inputs.utils_ref }} path: utils - - name: Setup - run: | - mkdir -p "$LOGS_DIR" - pip install mdxcanvas==${{ inputs.mdxcanvas_version }} discord-webhook + - name: Install MDXCanvas + run: pip install mdxcanvas==${{ inputs.mdxcanvas_version }} + + - name: Install notification dependencies + shell: bash + run: pip install discord-webhook markdowndata tabulate ${{ inputs.extra-packages }} + + - name: Create logs directory + run: mkdir -p "$LOGS_DIR" - name: Run MDXCanvas id: mdxcanvas @@ -80,36 +96,24 @@ jobs: > "$LOGS_DIR/mdxcanvas.stdout.log" \ 2> "$LOGS_DIR/mdxcanvas.stderr.log" - - name: Process results and notify - env: - DISCORD_WEBHOOK_URL: ${{ secrets.discord_webhook_url }} + - name: Print debug logs run: | echo "=== MDXCanvas STDOUT ===" cat "$LOGS_DIR/mdxcanvas.stdout.log" - + echo "=== MDXCanvas STDERR ===" cat "$LOGS_DIR/mdxcanvas.stderr.log" - - # Create fallback if needed - python utils/course_updates/create_fallback.py \ - --output-type MDXCanvas \ - --output-path "$OUTPUT_PATH" \ - --stdout-log "$LOGS_DIR/mdxcanvas.stdout.log" \ - --stderr-log "$LOGS_DIR/mdxcanvas.stderr.log" \ - --action-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - - echo "=== Updated Output ===" - cat "$OUTPUT_PATH" - - # Get avatar and send notification - AVATAR_URL=$(curl -s "https://api.github.com/users/${{ github.actor }}" | jq -r '.avatar_url') - - python utils/course_updates/send_course_notification.py \ - --type "canvas" \ - --payload "$OUTPUT_PATH" \ - --course-id "${{ inputs.course_id }}" \ - --author "${{ github.actor }}" \ - --author-icon "$AVATAR_URL" \ - --branch "${{ github.ref }}" \ - --cicd-id "${{ secrets.discord_role }}" \ - --action-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ No newline at end of file + + - name: Send course notification + uses: ./utils/.github/actions/send-course-notification + with: + notification-type: canvas + output-type: MDXCanvas + payload-path: ${{ env.OUTPUT_PATH }} + stdout-log: ${{ env.LOGS_DIR }}/mdxcanvas.stdout.log + stderr-log: ${{ env.LOGS_DIR }}/mdxcanvas.stderr.log + course-id: ${{ inputs.course_id }} + course-name: ${{ inputs.course_name }} + course-url: ${{ inputs.course_url }} + discord-role: ${{ secrets.discord_role }} + discord-webhook-url: ${{ secrets.discord_webhook_url }} diff --git a/.gitignore b/.gitignore index 7c7173d..04a9b5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc .idea/ -test_notification.py \ No newline at end of file +test_notification.py +.claude/worktrees/ \ No newline at end of file diff --git a/README.md b/README.md index b1fb779..e75b7ee 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,14 @@ on: jobs: docker_automation: - uses: BYU-CS-Course-Ops/utils/.github/workflows/docker_automation.yaml@main - with: - course_id: "235" - secrets: - discord_role: ${{ secrets.CICD_NOTIFY_DISCORD_ROLE }} - docker_user: ${{ secrets.DOCKER_USER }} + uses: BYU-CS-Course-Ops/utils/.github/workflows/docker_automation.yaml@main + with: + course_id: "235" + course_name: "CS 235 Spring 2025" + course_url: "https://example.com/courses/cs235" + secrets: + discord_role: ${{ secrets.CICD_NOTIFY_DISCORD_ROLE }} + docker_user: ${{ secrets.DOCKER_USER }} docker_password: ${{ secrets.DOCKER_PASSWORD }} discord_webhook_url: ${{ secrets.GHA_235_DISCORD_WEBHOOK }} ``` @@ -32,12 +34,14 @@ on: jobs: update-canvas: - uses: BYU-CS-Course-Ops/utils/.github/workflows/mdxcanvas_automation.yaml@main - with: - course_id: "235" - mdxcanvas_version: "0.3.0" - course_info_path: "_canvas-material/course-info/cs235_sp2025.json" - global_args_path: "_canvas-material/global_args.json" + uses: BYU-CS-Course-Ops/utils/.github/workflows/mdxcanvas_automation.yaml@main + with: + course_id: "235" + course_name: "CS 235 Spring 2025" + course_url: "https://example.com/courses/cs235" + mdxcanvas_version: "0.3.0" + course_info_path: "_canvas-material/course-info/cs235_sp2025.json" + global_args_path: "_canvas-material/global_args.json" canvas_css_path: "_canvas-material/canvas.css" # Optional template_path: "_canvas-material/course.canvas.md.xml.jinja" secrets: diff --git a/course_updates/canvas_notification.py b/course_updates/canvas_notification.py deleted file mode 100644 index 1e643f7..0000000 --- a/course_updates/canvas_notification.py +++ /dev/null @@ -1,109 +0,0 @@ -from datetime import datetime -from send_course_notification import Field, space, generate_field, truncate_error_message - -''' -Example payload (mdxcanvas -v 0.3.27): - -{ - "deployed_content": [ - [ - "file", - "beanlab.png", - null - ], - [ - "page", - "Example Page", - "https://byu.instructure.com/courses/20736/pages/example-page-13" - ] - ], - "content_to_review": [ - [ - "HW 7a - Set Performance", - "https://byu.instructure.com/courses/27547/quizzes/493001" - ] - ], - "error": "" -} -''' - -def requires_canvas_review(data) -> bool: - """ - Check if there is content to review in the payload. - This is used to determine if we should send a role mention. - """ - return data["content_to_review"] or data["error"] - - -def check_canvas_payload(data) -> bool: - """ - More specific check if we add more content types to the payload. - The notification is only sent if there is something to deploy, - review or if there is an error. - """ - return ( - data['deployed_content'] - or data['content_to_review'] - or data['error'] - ) - - -def canvas_format(data, course_id, author, author_icon, branch, action_url): - deployed_content = ( - '\n'.join(f'- **{rtype}**: [{content}]({link})' if link else f'- **{rtype}**: {content}' - for rtype, content, link in data['deployed_content'])) \ - if data['deployed_content'] \ - else '*No items deployed*' - - content_to_review = ( - '\n'.join(f'- [{dat[0]}]({dat[1]})' - for dat in data['content_to_review'])) \ - if data['content_to_review'] \ - else '*No items to review*' - - error = data["error"] if data['error'] else '*No errors*' - if error != '*No errors*': - error = truncate_error_message(error) - - return { - "username": "Canvas Notifications", - "avatar_url": "https://tinyurl.com/ek4ytkan", - "embeds": [{ - "author": {"name": author, "icon_url": author_icon}, - "title": f"CS {course_id} - Course Updates", - "description": f'**`{branch}`**', - "color": 15861021, - "fields": [ - space(), - *generate_field( - name='**Deployed Content:**', - value=deployed_content, - inline=False - ), - space(), - *generate_field( - name='**Content to Review:**', - value=content_to_review, - inline=False - ), - space(), - *generate_field( - name='**Error:**', - value=error, - inline=False - ), - space(), - Field( - name=f'**GitHub Action:**', - value=f'[View here]({action_url})', - inline=False - ), - space() - ], - "timestamp": datetime.now().isoformat(), - "footer": { - "text": "MDXCanvas GitHub Action", - "icon_url": "https://tinyurl.com/4ky2afzx" - } - }] - } diff --git a/course_updates/docker_notification.py b/course_updates/docker_notification.py deleted file mode 100644 index 271100b..0000000 --- a/course_updates/docker_notification.py +++ /dev/null @@ -1,95 +0,0 @@ -from datetime import datetime -from send_course_notification import Field, space, generate_field, truncate_error_message - -''' -Example payload (15 Apr 2025 - docker_notification.py): - -{ - "updated_images": [], - "failed_images": [], - "error": "" -} -''' - - -def requires_docker_review(data) -> bool: - """ - Check if there is content to review in the payload. - This is used to determine if we should send a role mention. - """ - return data["failed_images"] or data["error"] - - -def check_docker_payload(data) -> bool: - """ - More specific check if we add more content types to the payload. - The notification is only sent if there is an updated image, failed - image or if there is an error. - """ - return ( - data['updated_images'] - or data['failed_images'] - or data['error'] - ) - - -def docker_format(data, course_id, author, author_icon, branch, action_url): - updated_images = ( - '\n'.join(f'- {image}' - for image in data['updated_images'])) \ - if data['updated_images'] \ - else '*No updated images*' - - - failed_images = ( - '\n'.join(f'- {image}' - for image in data['failed_images'])) \ - if data['failed_images'] \ - else '*No items to review*' - - error = data["error"] if data['error'] else '*No errors*' - if error != '*No errors*': - error = truncate_error_message(error) - - return { - "username": "Gradescope Notifications", - "avatar_url": "https://tinyurl.com/mr2fyjse", - "embeds": [{ - "author": {"name": author, "icon_url": author_icon}, - "title": f"CS {course_id} - Docker Updates", - "description": f'**`{branch}`**', - "color": 238076, - "fields": [ - space(), - *generate_field( - name='**Updated Image(s):**', - value=updated_images, - inline=True - ), - space(), - *generate_field( - name='**Failed Image(s):**', - value=failed_images, - inline=True - ), - space(), - *generate_field( - name='**Error:**', - value=error, - inline=False - ), - space(), - Field( - name=f'**GitHub Action:**', - value=f'[View here]({action_url})', - inline=False - ), - space() - ], - "timestamp": datetime.now().isoformat(), - "footer": { - "text": "Docker GitHub Action", - "icon_url": "https://tinyurl.com/32ffdfss" - } - }] - } diff --git a/course_updates/send_course_notification.py b/course_updates/send_course_notification.py deleted file mode 100644 index 006afca..0000000 --- a/course_updates/send_course_notification.py +++ /dev/null @@ -1,223 +0,0 @@ -from argparse import ArgumentParser -import json -import os -from typing import TypedDict -from discord_webhook import DiscordWebhook, DiscordEmbed - - -class Field(TypedDict): - name: str - value: str - inline: bool - - -def space(inline=False) -> Field: - return Field(name="\u200b", value="\u200b", inline=inline) - - -def generate_field(name: str, value: str, inline: bool = False) -> list[Field]: - # Check if this is a code block - if value.startswith('```') and value.endswith('```'): - # Extract content from code block - lines = value.split('\n', 1) - if len(lines) > 1: - # Has content after opening ``` - rest = lines[1].rsplit('\n```', 1)[0] # Remove closing ``` - - if len(rest) > 1000: # Leave room for ``` markers - # Split content and wrap each chunk in code block - chunks = [] - chunk_size = 1000 - for i in range(0, len(rest), chunk_size): - chunk = rest[i:i + chunk_size] - chunks.append(f"```\n{chunk}\n```") - - return [ - Field( - name=name if i == 0 else f"{name} (continued)", - value=chunk_value, - inline=inline - ) for i, chunk_value in enumerate(chunks) - ] - - # Original logic for non-code-block values - if len(value) > 1024: - chunks = [value[i:i + 1024] for i in range(0, len(value), 1024)] - return [ - Field( - name=name if i == 0 else f"{name} (continued)", - value=chunk, - inline=inline - ) for i, chunk in enumerate(chunks) - ] - else: - return [Field(name=name, value=value, inline=inline)] - - -def truncate_error_message(error: str, max_chars: int = 900) -> str: - """ - Extracts the most relevant error information for Discord: - - If a traceback exists, keep only the last traceback - - Trim to the final few stack frames - - Always include the exception message - """ - - if not error: - return f"```\nNo error output available.\n```" - - lines = error.splitlines() - - # Find all traceback starts - traceback_indices = [ - i for i, line in enumerate(lines) - if line.strip().startswith("Traceback (most recent call last):") - ] - - if traceback_indices: - # Use only the LAST traceback - tb_start = traceback_indices[-1] - relevant = lines[tb_start:] - - # Keep only the last N stack frames + exception - # Stack frames usually come in pairs: File + code line - MAX_LINES = 12 - if len(relevant) > MAX_LINES: - relevant = ["... (traceback truncated) ..."] + relevant[-MAX_LINES:] - - message = "\n".join(relevant) - else: - # No traceback → take last meaningful chunk - message = "\n".join(lines[-15:]) - - # Hard cap for Discord - if len(message) > max_chars: - message = message[-max_chars:] - message = "... (truncated) ...\n" + message - - return f"```\n{message}\n```" - - - -def send_parsed_discord_embed(webhook_url: str, notification: dict, requires_review:bool, cicd_id: int = None): - webhook = DiscordWebhook( - url=webhook_url, - username=notification.get("username"), - avatar_url=notification.get("avatar_url"), - content=f"<@&{cicd_id}>" if requires_review and cicd_id else None, - ) - - for embed_data in notification.get("embeds", []): - embed = DiscordEmbed( - title=embed_data.get("title"), - description=embed_data.get("description"), - color=embed_data.get("color"), - timestamp=embed_data.get("timestamp") - ) - - # Optional author - author = embed_data.get("author", {}) - if author: - embed.set_author( - name=author.get("name", ""), - icon_url=author.get("icon_url", "") - ) - - # Optional footer - footer = embed_data.get("footer", {}) - if footer: - embed.set_footer( - text=footer.get("text", ""), - icon_url=footer.get("icon_url", "") - ) - - # Fields - for field in embed_data.get("fields", []): - field_name = field.get("name", "\u200b") - field_value = field.get("value", "\u200b") - - # Ensure neither name nor value is empty (Discord requirement) - if not field_name or not field_name.strip(): - field_name = "\u200b" - if not field_value or not field_value.strip(): - field_value = "\u200b" - - embed.add_embed_field( - name=field_name, - value=field_value, - inline=field.get("inline", False) - ) - - webhook.add_embed(embed) - - # Calculate approximate embed size - total_size = 0 - for embed_data in notification.get("embeds", []): - total_size += len(str(embed_data.get("title", ""))) - total_size += len(str(embed_data.get("description", ""))) - for field in embed_data.get("fields", []): - total_size += len(field.get("name", "")) - total_size += len(field.get("value", "")) - - if total_size > 4800: # 80% of 6000 limit - print(f"⚠️ Warning: Embed size ({total_size} chars) approaching Discord's 6000 char limit") - if total_size > 6000: - print(f"❌ Error: Embed size ({total_size} chars) exceeds Discord's 6000 char limit") - - response = webhook.execute() - if response.status_code >= 400: - print(f"❌ Discord returned status {response.status_code}: {response.text}") - else: - print("✅ Sent message successfully.") - - -def main(ntype, payload, course_id, author, author_icon, branch_name, action_url, cicd_id): - webhook_url = os.getenv("DISCORD_WEBHOOK_URL") - if not webhook_url: - raise EnvironmentError("DISCORD_WEBHOOK_URL environment variable is not set.") - - # Import formatter and checker - if ntype == "canvas": - from canvas_notification import canvas_format, check_canvas_payload, requires_canvas_review - format_notification = canvas_format - has_info = check_canvas_payload - requires_review = requires_canvas_review - elif ntype == "docker": - from docker_notification import docker_format, check_docker_payload, requires_docker_review - format_notification = docker_format - has_info = check_docker_payload - requires_review = requires_docker_review - else: - raise ValueError("Invalid notification type. Use 'canvas' or 'docker'.") - - with open(payload, 'r') as file: - data = json.load(file) - - if not data or not has_info(data): - print("No information to send.") - return - - notification = format_notification( - data=data, - course_id=course_id, - author=author, - author_icon=author_icon, - branch=branch_name, - action_url=action_url, - ) - - send_parsed_discord_embed(webhook_url, notification, requires_review(data), cicd_id=cicd_id) - -if __name__ == "__main__": - parser = ArgumentParser(description="Send Canvas or Docker notifications to Discord.") - parser.add_argument("--type", required=True, choices=["canvas", "docker"], help="Type of notification") - parser.add_argument("--payload", required=True, help="Path to the payload JSON file") - parser.add_argument("--course-id", required=True, help="Course ID") - parser.add_argument("--author", required=True, help="Name of the author") - parser.add_argument("--author-icon", required=True, help="URL of the author's icon") - parser.add_argument("--branch", required=True, help="Branch name") - parser.add_argument("--action-url", required=True, help="URL to the GHA") - parser.add_argument("--cicd-id", nargs='?', const=None, default=None, help="CI/CD Role ID") - - args = parser.parse_args() - - main(args.type, args.payload, args.course_id, args.author, args.author_icon, args.branch, args.action_url, args.cicd_id) diff --git a/notifications/__init__.py b/notifications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/notifications/discord_limits.py b/notifications/discord_limits.py new file mode 100644 index 0000000..8755bf5 --- /dev/null +++ b/notifications/discord_limits.py @@ -0,0 +1,249 @@ +from __future__ import annotations + +from .resources import Author, Embed, Field, Footer, Notification, WebhookMessage + +# -- Discord API limits ------------------------------------------------------- +TITLE_LIMIT = 256 +DESCRIPTION_LIMIT = 4096 +FIELD_NAME_LIMIT = 256 +FIELD_VALUE_LIMIT = 1024 +FIELDS_PER_EMBED = 25 +FOOTER_LIMIT = 2048 +AUTHOR_NAME_LIMIT = 256 +EMBED_CHAR_LIMIT = 6000 +MAX_EMBEDS_PER_MESSAGE = 10 +CONTENT_LIMIT = 2000 + + +def calc_embed_size(embed: Embed) -> int: + size = 0 + size += len(embed.title or "") + size += len(embed.description or "") + if embed.author: + size += len(embed.author.name or "") + if embed.footer: + size += len(embed.footer.text or "") + for field in embed.fields: + size += len(field.name or "") + size += len(field.value or "") + return size + + +def split_content(content: str, max_chars: int = CONTENT_LIMIT) -> list[str]: + if len(content) <= max_chars: + return [content] + + paragraphs = content.split("\n\n") + chunks: list[str] = [] + current_parts: list[str] = [] + current_length = 0 + + for paragraph in paragraphs: + needed = len(paragraph) + (2 if current_parts else 0) + if current_parts and current_length + needed > max_chars: + chunks.append("\n\n".join(current_parts)) + current_parts = [paragraph] + current_length = len(paragraph) + continue + current_parts.append(paragraph) + current_length += needed + + if current_parts: + chunks.append("\n\n".join(current_parts)) + + return chunks + + +# -- EmbedBuilder ------------------------------------------------------------- + +class EmbedBuilder: + def __init__( + self, + title: str, + description: str, + color: int, + timestamp: str, + author: Author | None = None, + footer: Footer | None = None, + ): + self._title = title + self._description = description + self._color = color + self._timestamp = timestamp + self._author = author + self._footer = footer + self._fields: list[Field] = [] + + self._base_size = len(title or "") + len(description or "") + if author: + self._base_size += len(author.name or "") + if footer: + self._base_size += len(footer.text or "") + self._fields_size = 0 + + def remaining_chars(self) -> int: + return EMBED_CHAR_LIMIT - self._base_size - self._fields_size + + def can_add_field(self, name: str, value: str) -> bool: + if len(self._fields) >= FIELDS_PER_EMBED: + return False + field_size = len(name or "") + len(value or "") + return field_size <= self.remaining_chars() + + def add_field(self, name: str, value: str, inline: bool = False) -> bool: + if not self.can_add_field(name, value): + return False + self._fields.append(Field(name=name, value=value, inline=inline)) + self._fields_size += len(name or "") + len(value or "") + return True + + def build(self) -> Embed: + return Embed( + title=self._title, + description=self._description, + color=self._color, + fields=list(self._fields), + timestamp=self._timestamp, + author=self._author, + footer=self._footer, + ) + + +# -- MessageBuilder ----------------------------------------------------------- + +class MessageBuilder: + def __init__(self, color: int, timestamp: str): + self._color = color + self._timestamp = timestamp + self._content: str | None = None + self._embeds: list[EmbedBuilder] = [] + + def set_content(self, content: str): + self._content = content + + @property + def current_embed(self) -> EmbedBuilder | None: + return self._embeds[-1] if self._embeds else None + + def new_embed( + self, + title: str, + description: str, + author: Author | None = None, + footer: Footer | None = None, + ) -> EmbedBuilder: + eb = EmbedBuilder( + title=title, + description=description, + color=self._color, + timestamp=self._timestamp, + author=author, + footer=footer, + ) + self._embeds.append(eb) + return eb + + def build(self) -> list[WebhookMessage]: + """Build webhook messages, grouping embeds so each message's + combined embed character total stays within EMBED_CHAR_LIMIT + and each message has at most MAX_EMBEDS_PER_MESSAGE embeds.""" + built_embeds = [eb.build() for eb in self._embeds] + messages: list[WebhookMessage] = [] + current_embeds: list[Embed] = [] + current_size = 0 + + for embed in built_embeds: + embed_size = calc_embed_size(embed) + would_exceed_chars = current_embeds and current_size + embed_size > EMBED_CHAR_LIMIT + would_exceed_count = len(current_embeds) >= MAX_EMBEDS_PER_MESSAGE + + if current_embeds and (would_exceed_chars or would_exceed_count): + messages.append(WebhookMessage( + content=self._content if not messages else None, + embeds=current_embeds, + )) + current_embeds = [] + current_size = 0 + + current_embeds.append(embed) + current_size += embed_size + + if current_embeds: + messages.append(WebhookMessage( + content=self._content if not messages else None, + embeds=current_embeds, + )) + + return messages if messages else [WebhookMessage(content=self._content, embeds=[])] + + +# -- validate_notification ---------------------------------------------------- + +def validate_notification(notification: Notification) -> list[str]: + violations: list[str] = [] + + for msg_idx, message in enumerate(notification.messages): + prefix = f"Message {msg_idx}" + + if message.content and len(message.content) > CONTENT_LIMIT: + violations.append( + f"{prefix}: Content length {len(message.content)} exceeds {CONTENT_LIMIT}" + ) + + if len(message.embeds) > MAX_EMBEDS_PER_MESSAGE: + violations.append( + f"{prefix}: Embed count {len(message.embeds)} exceeds {MAX_EMBEDS_PER_MESSAGE}" + ) + + for emb_idx, embed in enumerate(message.embeds): + ep = f"{prefix}, Embed {emb_idx}" + + if embed.title and len(embed.title) > TITLE_LIMIT: + violations.append( + f"{ep}: Title length {len(embed.title)} exceeds {TITLE_LIMIT}" + ) + + if embed.description and len(embed.description) > DESCRIPTION_LIMIT: + violations.append( + f"{ep}: Description length {len(embed.description)} exceeds {DESCRIPTION_LIMIT}" + ) + + if embed.footer and len(embed.footer.text or "") > FOOTER_LIMIT: + violations.append( + f"{ep}: Footer length {len(embed.footer.text)} exceeds {FOOTER_LIMIT}" + ) + + if embed.author and len(embed.author.name or "") > AUTHOR_NAME_LIMIT: + violations.append( + f"{ep}: Author name length {len(embed.author.name)} exceeds {AUTHOR_NAME_LIMIT}" + ) + + if len(embed.fields) > FIELDS_PER_EMBED: + violations.append( + f"{ep}: Field count {len(embed.fields)} exceeds {FIELDS_PER_EMBED}" + ) + + for fld_idx, field in enumerate(embed.fields): + if field.name and len(field.name) > FIELD_NAME_LIMIT: + violations.append( + f"{ep}, Field {fld_idx}: Field name length {len(field.name)} exceeds {FIELD_NAME_LIMIT}" + ) + if field.value and len(field.value) > FIELD_VALUE_LIMIT: + violations.append( + f"{ep}, Field {fld_idx}: Field value length {len(field.value)} exceeds {FIELD_VALUE_LIMIT}" + ) + + total = calc_embed_size(embed) + if total > EMBED_CHAR_LIMIT: + violations.append( + f"{ep}: Embed char total {total} exceeds {EMBED_CHAR_LIMIT}" + ) + + # Combined embed size across all embeds in a single message + combined = sum(calc_embed_size(e) for e in message.embeds) + if combined > EMBED_CHAR_LIMIT: + violations.append( + f"{prefix}: Combined embed char total {combined} exceeds {EMBED_CHAR_LIMIT}" + ) + + return violations diff --git a/course_updates/create_fallback.py b/notifications/fallback.py similarity index 100% rename from course_updates/create_fallback.py rename to notifications/fallback.py diff --git a/notifications/formatting/__init__.py b/notifications/formatting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/notifications/formatting/canvas_format.py b/notifications/formatting/canvas_format.py new file mode 100644 index 0000000..32886a2 --- /dev/null +++ b/notifications/formatting/canvas_format.py @@ -0,0 +1,203 @@ +from __future__ import annotations + +from collections import Counter, defaultdict +from datetime import datetime, timezone + +from tabulate import tabulate + +from notifications.discord_limits import FIELD_VALUE_LIMIT, EmbedBuilder, MessageBuilder +from notifications.formatting.formatting_utils import get_course_style, truncate_error +from notifications.formatting.plain_text_utils import ( + dedupe_remaining_content, + status_color, +) +from notifications.resources import Author, Footer, Notification + + +def has_content(data) -> bool: + return bool( + data["deployed_content"] + or data["content_to_review"] + or data["error"] + ) + + +def requires_review(data) -> bool: + return bool(data["content_to_review"] or data["error"]) + + +def _build_overview_table(deployed_content: list, content_to_review: list) -> str: + """Build a tabulate overview table counting distinct items by resource type.""" + seen: set[str] = set() + type_counts: Counter = Counter() + + for content_type, name, *_ in deployed_content: + if name not in seen: + seen.add(name) + type_counts[content_type] += 1 + + for content_type, name, *_ in content_to_review: + if name not in seen: + seen.add(name) + type_counts[content_type] += 1 + + rows = sorted(type_counts.items(), key=lambda r: (-r[1], r[0])) + table = tabulate(rows, headers=["Resource Type", "Count"], tablefmt="presto") + return f"```\n{table}\n```" + + +def _format_item(resource_type: str, name: str, max_len: int, link: str | None) -> str: + label = f"`{resource_type}`" + if link: + link = f"[{name}]({link})" + return f"{label:{max_len}} {link:<3}" + return f"{label:{max_len}} {name:<3}" + + +def _add_items_to_builder( + builder: EmbedBuilder, + message_builder: MessageBuilder, + header: str, + lines: list[str], + author: Author | None, + footer: Footer | None, + continuation_title: str, +): + """Pack item lines into fields, starting new embeds as needed.""" + current_lines: list[str] = [] + is_first_field = True + + def flush_field(): + nonlocal current_lines, is_first_field, builder + if not current_lines: + return + value = "\n".join(current_lines) + name = header if is_first_field else "\u200b" + + if not builder.can_add_field(name, value): + builder = message_builder.new_embed( + title=continuation_title, + description="", + footer=footer, + ) + + builder.add_field(name, value, inline=False) + is_first_field = False + current_lines = [] + + for line in lines: + test_value = "\n".join(current_lines + [line]) + test_name = header if is_first_field else "\u200b" + + would_exceed_field = len(test_value) > FIELD_VALUE_LIMIT + would_exceed_embed = not builder.can_add_field(test_name, test_value) + + if current_lines and (would_exceed_field or would_exceed_embed): + flush_field() + + current_lines.append(line) + + flush_field() + return builder + + +def format_notification( + data, + course_id, + course_name, + course_url, + author, + author_icon, + branch, + action_url, + cicd_role_id=None, +) -> Notification: + style = get_course_style("canvas") + timestamp = datetime.now(timezone.utc).isoformat() + footer = Footer(text=style["footer_text"], icon_url=style["footer_icon_url"]) + author_obj = Author(name=author, icon_url=author_icon) + + # -- Title ---------------------------------------------------------------- + if data["error"]: + title = f"CS {course_id} | {course_name} -- Deploy failed" + elif data["content_to_review"]: + title = f"CS {course_id} | {course_name} -- Deploy complete -- items need review" + else: + title = f"CS {course_id} | {course_name} -- Deploy complete" + + continuation_title = f"{title} (continued)" + + # -- Color ---------------------------------------------------------------- + color = status_color( + has_error=bool(data["error"]), + needs_review=requires_review(data), + ) + + # -- Content message ------------------------------------------------------ + content = None + if data["error"] and cicd_role_id: + content = f"<@&{cicd_role_id}> ERROR -- MDXCanvas failed to deploy. View [here]({action_url})" + elif data["content_to_review"] and cicd_role_id: + content = f"<@&{cicd_role_id}> -- Deployed Resources to Review" + + # -- Description ---------------------------------------------------------- + description = f"**Branch:** `{branch}`" + + if data["error"]: + truncated = truncate_error(data["error"]) + description += f"\n\n**Error:**\n{truncated}" + + # -- Build message -------------------------------------------------------- + mb = MessageBuilder(color=color, timestamp=timestamp) + if content: + mb.set_content(content) + + eb = mb.new_embed( + title=title, + description=description, + author=author_obj, + footer=footer, + ) + + # -- Calculate max resource type length for formatting --------------------------------------------- + grouped = defaultdict(int) + for rtype, _, _ in data["deployed_content"]: + grouped[rtype] += 1 + + max_len = max((len(rtype) for rtype in grouped), default=0) + + # -- Overview table ------------------------------------------------------- + if data["deployed_content"] or data["content_to_review"]: + table = _build_overview_table(data["deployed_content"], data["content_to_review"]) + eb.add_field("Overview:", table, inline=False) + + # -- Needs review items --------------------------------------------------- + if data["content_to_review"]: + lines = [ + _format_item(content_type, name, max_len, url) + for content_type, name, url in data["content_to_review"] + ] + header = f"Needs Review ({len(data['content_to_review'])})" + _add_items_to_builder( + eb, mb, header, lines, author_obj, footer, continuation_title, + ) + + # -- Remaining resources -------------------------------------------------- + remaining = dedupe_remaining_content( + data["deployed_content"], data["content_to_review"] + ) + if remaining: + lines = [ + _format_item(content_type, name, max_len, url) + for content_type, name, url in remaining + ] + header = f"Remaining Resources ({len(remaining)})" + _add_items_to_builder( + eb, mb, header, lines, author_obj, footer, continuation_title, + ) + + return Notification( + username=style["username"], + avatar_url=style["avatar_url"], + messages=mb.build(), + ) diff --git a/notifications/formatting/docker_format.py b/notifications/formatting/docker_format.py new file mode 100644 index 0000000..7ec8a54 --- /dev/null +++ b/notifications/formatting/docker_format.py @@ -0,0 +1,143 @@ +from datetime import datetime, timezone + +from notifications.discord_limits import FIELD_VALUE_LIMIT, EmbedBuilder, MessageBuilder +from notifications.formatting.formatting_utils import get_course_style, truncate_error +from notifications.formatting.plain_text_utils import status_color +from notifications.resources import Author, Footer, Notification + + +def has_content(data) -> bool: + return bool( + data["updated_images"] + or data["failed_images"] + or data["error"] + ) + + +def requires_review(data) -> bool: + return bool(data["failed_images"] or data["error"]) + +def _add_items_to_builder( + builder: EmbedBuilder, + message_builder: MessageBuilder, + header: str, + lines: list[str], + author: Author | None, + footer: Footer | None, + continuation_title: str, +): + """Pack item lines into fields, starting new embeds as needed.""" + current_lines: list[str] = [] + is_first_field = True + + def flush_field(): + nonlocal current_lines, is_first_field, builder + if not current_lines: + return + value = "\n".join(current_lines) + name = header if is_first_field else "\u200b" + + if not builder.can_add_field(name, value): + builder = message_builder.new_embed( + title=continuation_title, + description="", + footer=footer, + ) + + builder.add_field(name, value, inline=False) + is_first_field = False + current_lines = [] + + for line in lines: + test_value = "\n".join(current_lines + [line]) + test_name = header if is_first_field else "\u200b" + + would_exceed_field = len(test_value) > FIELD_VALUE_LIMIT + would_exceed_embed = not builder.can_add_field(test_name, test_value) + + if current_lines and (would_exceed_field or would_exceed_embed): + flush_field() + + current_lines.append(line) + + flush_field() + return builder + + +def format_notification( + data, + course_id, + course_name, + course_url, + author, + author_icon, + branch, + action_url, + cicd_role_id=None, +) -> Notification: + style = get_course_style("docker") + timestamp = datetime.now(timezone.utc).isoformat() + footer = Footer(text=style["footer_text"], icon_url=style["footer_icon_url"]) + author_obj = Author(name=author, icon_url=author_icon) + + # -- Title ---------------------------------------------------------------- + if data["error"]: + title = f"CS {course_id} | {course_name} -- Build failed" + elif data["failed_images"]: + title = f"CS {course_id} | {course_name} -- Build complete -- failures" + else: + title = f"CS {course_id} | {course_name} -- Build complete" + + continuation_title = f"{title} (continued)" + + # -- Color ---------------------------------------------------------------- + color = status_color( + has_error=bool(data["error"]), + needs_review=requires_review(data), + ) + + # -- Content message ------------------------------------------------------ + content = None + if data["error"] and cicd_role_id: + content = f"<@&{cicd_role_id}> ERROR -- Docker failed build(s). View [here]({action_url})" + elif data["failed_images"] and cicd_role_id: + content = f"<@&{cicd_role_id}> -- Docker build complete with failures -- review failed images" + + # -- Description ---------------------------------------------------------- + description = f"**Branch:** `{branch}`" + + if data["error"]: + truncated = truncate_error(data["error"]) + description += f"\n\n**Error:**\n{truncated}" + + # -- Build message -------------------------------------------------------- + mb = MessageBuilder(color=color, timestamp=timestamp) + if content: + mb.set_content(content) + + eb = mb.new_embed( + title=title, + description=description, + author=author_obj, + footer=footer, + ) + + if data["failed_images"]: + lines = [f"`{image}`" for image in data["failed_images"]] + header = f"Build(s) Failed ({len(data['failed_images'])})" + _add_items_to_builder( + eb, mb, header, lines, author_obj, footer, continuation_title, + ) + + if data["updated_images"]: + lines = [f"`{image}`" for image in data["updated_images"]] + header = f"Successfully Built ({len(data['updated_images'])})" + _add_items_to_builder( + eb, mb, header, lines, author_obj, footer, continuation_title, + ) + + return Notification( + username=style["username"], + avatar_url=style["avatar_url"], + messages=mb.build(), + ) diff --git a/notifications/formatting/formatting_utils.py b/notifications/formatting/formatting_utils.py new file mode 100644 index 0000000..b9196de --- /dev/null +++ b/notifications/formatting/formatting_utils.py @@ -0,0 +1,52 @@ +from markdowndata import load +from pathlib import Path + + +STYLE_PATH = Path(__file__).parent / "style.md" + + +def hex_to_int(hex_color: str) -> int: + return int(hex_color.lstrip('#'), 16) + + +def _load_styles(): + with open(STYLE_PATH) as f: + return load(f) + + +def get_course_style(ntype: str) -> dict[str, str]: + styles = _load_styles() + for row in styles.get("Course", []): + if row.get("type") == ntype: + return row + return {} + + +def truncate_error(error: str, max_chars: int = 900) -> str: + if not error: + return "```\nNo error output available.\n```" + + lines = error.splitlines() + + traceback_indices = [ + i for i, line in enumerate(lines) + if line.strip().startswith("Traceback (most recent call last):") + ] + + if traceback_indices: + tb_start = traceback_indices[-1] + relevant = lines[tb_start:] + + MAX_LINES = 12 + if len(relevant) > MAX_LINES: + relevant = ["... (traceback truncated) ..."] + relevant[-MAX_LINES:] + + message = "\n".join(relevant) + else: + message = "\n".join(lines[-15:]) + + if len(message) > max_chars: + message = message[-max_chars:] + message = "... (truncated) ...\n" + message + + return f"```\n{message}\n```" diff --git a/notifications/formatting/plain_text_utils.py b/notifications/formatting/plain_text_utils.py new file mode 100644 index 0000000..177ec26 --- /dev/null +++ b/notifications/formatting/plain_text_utils.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +SUCCESS_COLOR = 0x57F287 # Green +REVIEW_COLOR = 0xFEE75C # Yellow +ERROR_COLOR = 0xED4245 # Red + + +def status_color(*, has_error: bool, needs_review: bool) -> int: + if has_error: + return ERROR_COLOR + if needs_review: + return REVIEW_COLOR + return SUCCESS_COLOR + + +def dedupe_remaining_content( + deployed_content: list[tuple[str, str, str | None]], + review_items: list[tuple[str, str, str | None]], +) -> list[tuple[str, str, str | None]]: + review_urls = {url for _, _, url in review_items if url} + review_labels = {label for _, label, _ in review_items} + + seen_urls: set[str] = set() + seen_labels: set[str] = set() + remaining = [] + + for content_type, label, url in deployed_content: + if url and url in review_urls: + continue + if label in review_labels: + continue + if url and url in seen_urls: + continue + if label in seen_labels: + continue + + if url: + seen_urls.add(url) + seen_labels.add(label) + remaining.append((content_type, label, url)) + + return remaining diff --git a/notifications/formatting/style.md b/notifications/formatting/style.md new file mode 100644 index 0000000..a3d983d --- /dev/null +++ b/notifications/formatting/style.md @@ -0,0 +1,6 @@ +# Course + +| type | username | avatar_url | hex_color | title_template | footer_text | footer_icon_url | +|--------|--------------------------|------------------------------|-----------|-------------------------------------|-------------------------|------------------------------| +| canvas | Canvas Notifications | https://tinyurl.com/ek4ytkan | #F2051D | CS {course_id} - Course Updates | MDXCanvas GitHub Action | https://tinyurl.com/4ky2afzx | +| docker | Gradescope Notifications | https://tinyurl.com/mr2fyjse | #03A1FC | CS {course_id} - Docker Updates | Docker GitHub Action | https://tinyurl.com/4ky2afzx | diff --git a/notifications/resources.py b/notifications/resources.py new file mode 100644 index 0000000..80e6a9f --- /dev/null +++ b/notifications/resources.py @@ -0,0 +1,46 @@ +from dataclasses import dataclass, field + + +@dataclass +class Field: + name: str + value: str + inline: bool = False + + +@dataclass +class Author: + name: str + icon_url: str = "" + + +@dataclass +class Footer: + text: str + icon_url: str = "" + + +@dataclass +class Embed: + title: str + description: str + color: int + fields: list[Field] + timestamp: str + author: Author | None = None + footer: Footer | None = None + + +@dataclass +class WebhookMessage: + content: str | None = None + embeds: list[Embed] = field(default_factory=list) + sections: list[str] = field(default_factory=list) + continuation_title: str | None = None + + +@dataclass +class Notification: + username: str + messages: list[WebhookMessage] + avatar_url: str | None = None diff --git a/notifications/send_course.py b/notifications/send_course.py new file mode 100644 index 0000000..ba0c0ce --- /dev/null +++ b/notifications/send_course.py @@ -0,0 +1,73 @@ +import json +import os +from argparse import ArgumentParser + +from .formatting import canvas_format, docker_format +from .send_notification import send_notification + + +FORMATTERS = { + "canvas": (canvas_format.format_notification, canvas_format.has_content), + "docker": (docker_format.format_notification, docker_format.has_content), +} + + +def main(ntype, payload, course_id, course_name, course_url, author, author_icon, branch_name, action_url, cicd_role_id): + webhook_url = os.getenv("DISCORD_WEBHOOK_URL") + if not webhook_url: + raise EnvironmentError("DISCORD_WEBHOOK_URL environment variable is not set.") + + if ntype not in FORMATTERS: + raise ValueError("Invalid notification type. Use 'canvas' or 'docker'.") + + format_notification, has_content = FORMATTERS[ntype] + + with open(payload, 'r') as file: + data = json.load(file) + + if not data or not has_content(data): + print("No information to send.") + return + + notification = format_notification( + data=data, + course_id=course_id, + course_name=course_name, + course_url=course_url, + author=author, + author_icon=author_icon or "", + branch=branch_name, + action_url=action_url, + cicd_role_id=cicd_role_id, + ) + + send_notification(webhook_url, notification) + + +if __name__ == "__main__": + parser = ArgumentParser(description="Send Canvas or Docker notifications to Discord.") + parser.add_argument("--type", required=True, choices=["canvas", "docker"], help="Type of notification") + parser.add_argument("--payload", required=True, help="Path to the payload JSON file") + parser.add_argument("--course-id", required=True, help="Course ID") + parser.add_argument("--course-name", required=True, help="Course name") + parser.add_argument("--course-url", required=True, help="Course URL") + parser.add_argument("--author", required=True, help="Name of the author") + parser.add_argument("--branch", required=True, help="Branch name") + parser.add_argument("--action-url", required=True, help="URL to the GHA") + parser.add_argument("--author-icon", nargs='?', const=None, default=None, help="Author avatar URL") + parser.add_argument("--cicd-id", nargs='?', const=None, default=None, help="CI/CD Role ID") + + args = parser.parse_args() + + main( + args.type, + args.payload, + args.course_id, + args.course_name, + args.course_url, + args.author, + args.author_icon, + args.branch, + args.action_url, + args.cicd_id, + ) diff --git a/notifications/send_notification.py b/notifications/send_notification.py new file mode 100644 index 0000000..f492c76 --- /dev/null +++ b/notifications/send_notification.py @@ -0,0 +1,70 @@ +from discord_webhook import DiscordWebhook, DiscordEmbed + +from .discord_limits import CONTENT_LIMIT, split_content +from .resources import Embed, Notification + + +def _build_discord_embed(embed_data: Embed) -> DiscordEmbed: + embed = DiscordEmbed( + title=embed_data.title, + description=embed_data.description, + color=embed_data.color, + timestamp=embed_data.timestamp or None, + ) + + if embed_data.author: + embed.set_author( + name=embed_data.author.name, + icon_url=embed_data.author.icon_url, + ) + + if embed_data.footer: + embed.set_footer( + text=embed_data.footer.text, + icon_url=embed_data.footer.icon_url, + ) + + for field in embed_data.fields: + embed.add_embed_field( + name=field.name or "\u200b", + value=field.value or "\u200b", + inline=field.inline, + ) + + return embed + + +def _execute_webhook(webhook_url: str, notification: Notification, content: str | None = None, embeds: list[Embed] | None = None): + webhook = DiscordWebhook( + url=webhook_url, + username=notification.username, + avatar_url=notification.avatar_url, + content=content, + ) + + for embed_data in embeds or []: + webhook.add_embed(_build_discord_embed(embed_data)) + + return webhook.execute() + + +def send_notification(webhook_url: str, notification: Notification): + for message in notification.messages: + content_chunks = split_content(message.content, CONTENT_LIMIT) if message.content else [None] + + for index, content in enumerate(content_chunks): + try: + response = _execute_webhook( + webhook_url=webhook_url, + notification=notification, + content=content, + embeds=message.embeds if index == 0 else [], + ) + except Exception as e: + print(f"Error sending notification: {e}") + continue + + if response.status_code >= 400: + print(f"Discord returned status {response.status_code}: {response.text}") + else: + print("Sent message successfully.") diff --git a/tests/failed-mdxcanvas-notification.png b/tests/failed-mdxcanvas-notification.png new file mode 100644 index 0000000..8e19f3b Binary files /dev/null and b/tests/failed-mdxcanvas-notification.png differ diff --git a/tests/send_test_notification.py b/tests/send_test_notification.py new file mode 100644 index 0000000..f8516d1 --- /dev/null +++ b/tests/send_test_notification.py @@ -0,0 +1,40 @@ +""" +Send a test Discord notification from a JSON payload file. + +Usage: + source ~/.env + python send_test_notification.py --type canvas --payload path/to/payload.json + python send_test_notification.py --type docker --payload path/to/payload.json +""" + +import argparse +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from notifications.send_course import main as send_course + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Send a test Discord notification.") + parser.add_argument("--type", required=True, choices=["canvas", "docker"]) + parser.add_argument("--payload", required=True, help="Path to payload JSON file") + args = parser.parse_args() + + if not os.path.isfile(args.payload): + print(f"Error: payload file not found at {args.payload}", file=sys.stderr) + sys.exit(1) + + send_course( + ntype=args.type, + payload=args.payload, + course_id="000", + course_name="Test Course", + course_url="https://example.com", + author="test-user", + author_icon="https://github.com/ghost.png", + branch_name="test-branch", + action_url="https://github.com/actions/runs/0", + cicd_role_id=1373077186072019024, + ) diff --git a/tests/success-mdxcanvas-notification.png b/tests/success-mdxcanvas-notification.png new file mode 100644 index 0000000..7a750f8 Binary files /dev/null and b/tests/success-mdxcanvas-notification.png differ diff --git a/tests/test-docker-payload.json b/tests/test-docker-payload.json new file mode 100644 index 0000000..bac7149 --- /dev/null +++ b/tests/test-docker-payload.json @@ -0,0 +1,507 @@ +{ + "updated_images": [ + "build_test_1_docker.sh", + "build_test_2_docker.sh", + "build_test_3_docker.sh", + "build_test_4_docker.sh", + "build_test_5_docker.sh", + "build_test_6_docker.sh", + "build_test_7_docker.sh", + "build_test_8_docker.sh", + "build_test_9_docker.sh", + "build_test_10_docker.sh", + "build_test_11_docker.sh", + "build_test_12_docker.sh", + "build_test_13_docker.sh", + "build_test_14_docker.sh", + "build_test_15_docker.sh", + "build_test_16_docker.sh", + "build_test_17_docker.sh", + "build_test_18_docker.sh", + "build_test_19_docker.sh", + "build_test_20_docker.sh", + "build_test_21_docker.sh", + "build_test_22_docker.sh", + "build_test_23_docker.sh", + "build_test_24_docker.sh", + "build_test_25_docker.sh", + "build_test_26_docker.sh", + "build_test_27_docker.sh", + "build_test_28_docker.sh", + "build_test_29_docker.sh", + "build_test_30_docker.sh", + "build_test_31_docker.sh", + "build_test_32_docker.sh", + "build_test_33_docker.sh", + "build_test_34_docker.sh", + "build_test_35_docker.sh", + "build_test_36_docker.sh", + "build_test_37_docker.sh", + "build_test_38_docker.sh", + "build_test_39_docker.sh", + "build_test_40_docker.sh", + "build_test_41_docker.sh", + "build_test_42_docker.sh", + "build_test_43_docker.sh", + "build_test_44_docker.sh", + "build_test_45_docker.sh", + "build_test_46_docker.sh", + "build_test_47_docker.sh", + "build_test_48_docker.sh", + "build_test_49_docker.sh", + "build_test_50_docker.sh", + "build_test_51_docker.sh", + "build_test_52_docker.sh", + "build_test_53_docker.sh", + "build_test_54_docker.sh", + "build_test_55_docker.sh", + "build_test_56_docker.sh", + "build_test_57_docker.sh", + "build_test_58_docker.sh", + "build_test_59_docker.sh", + "build_test_60_docker.sh", + "build_test_61_docker.sh", + "build_test_62_docker.sh", + "build_test_63_docker.sh", + "build_test_64_docker.sh", + "build_test_65_docker.sh", + "build_test_66_docker.sh", + "build_test_67_docker.sh", + "build_test_68_docker.sh", + "build_test_69_docker.sh", + "build_test_70_docker.sh", + "build_test_71_docker.sh", + "build_test_72_docker.sh", + "build_test_73_docker.sh", + "build_test_74_docker.sh", + "build_test_75_docker.sh", + "build_test_76_docker.sh", + "build_test_77_docker.sh", + "build_test_78_docker.sh", + "build_test_79_docker.sh", + "build_test_80_docker.sh", + "build_test_81_docker.sh", + "build_test_82_docker.sh", + "build_test_83_docker.sh", + "build_test_84_docker.sh", + "build_test_85_docker.sh", + "build_test_86_docker.sh", + "build_test_87_docker.sh", + "build_test_88_docker.sh", + "build_test_89_docker.sh", + "build_test_90_docker.sh", + "build_test_91_docker.sh", + "build_test_92_docker.sh", + "build_test_93_docker.sh", + "build_test_94_docker.sh", + "build_test_95_docker.sh", + "build_test_96_docker.sh", + "build_test_97_docker.sh", + "build_test_98_docker.sh", + "build_test_99_docker.sh", + "build_test_100_docker.sh", + "build_test_1_docker.sh", + "build_test_2_docker.sh", + "build_test_3_docker.sh", + "build_test_4_docker.sh", + "build_test_5_docker.sh", + "build_test_6_docker.sh", + "build_test_7_docker.sh", + "build_test_8_docker.sh", + "build_test_9_docker.sh", + "build_test_10_docker.sh", + "build_test_11_docker.sh", + "build_test_12_docker.sh", + "build_test_13_docker.sh", + "build_test_14_docker.sh", + "build_test_15_docker.sh", + "build_test_16_docker.sh", + "build_test_17_docker.sh", + "build_test_18_docker.sh", + "build_test_19_docker.sh", + "build_test_20_docker.sh", + "build_test_21_docker.sh", + "build_test_22_docker.sh", + "build_test_23_docker.sh", + "build_test_24_docker.sh", + "build_test_25_docker.sh", + "build_test_26_docker.sh", + "build_test_27_docker.sh", + "build_test_28_docker.sh", + "build_test_29_docker.sh", + "build_test_30_docker.sh", + "build_test_31_docker.sh", + "build_test_32_docker.sh", + "build_test_33_docker.sh", + "build_test_34_docker.sh", + "build_test_35_docker.sh", + "build_test_36_docker.sh", + "build_test_37_docker.sh", + "build_test_38_docker.sh", + "build_test_39_docker.sh", + "build_test_40_docker.sh", + "build_test_41_docker.sh", + "build_test_42_docker.sh", + "build_test_43_docker.sh", + "build_test_44_docker.sh", + "build_test_45_docker.sh", + "build_test_46_docker.sh", + "build_test_47_docker.sh", + "build_test_48_docker.sh", + "build_test_49_docker.sh", + "build_test_50_docker.sh", + "build_test_51_docker.sh", + "build_test_52_docker.sh", + "build_test_53_docker.sh", + "build_test_54_docker.sh", + "build_test_55_docker.sh", + "build_test_56_docker.sh", + "build_test_57_docker.sh", + "build_test_58_docker.sh", + "build_test_59_docker.sh", + "build_test_60_docker.sh", + "build_test_61_docker.sh", + "build_test_62_docker.sh", + "build_test_63_docker.sh", + "build_test_64_docker.sh", + "build_test_65_docker.sh", + "build_test_66_docker.sh", + "build_test_67_docker.sh", + "build_test_68_docker.sh", + "build_test_69_docker.sh", + "build_test_70_docker.sh", + "build_test_71_docker.sh", + "build_test_72_docker.sh", + "build_test_73_docker.sh", + "build_test_74_docker.sh", + "build_test_75_docker.sh", + "build_test_76_docker.sh", + "build_test_77_docker.sh", + "build_test_78_docker.sh", + "build_test_79_docker.sh", + "build_test_80_docker.sh", + "build_test_81_docker.sh", + "build_test_82_docker.sh", + "build_test_83_docker.sh", + "build_test_84_docker.sh", + "build_test_85_docker.sh", + "build_test_86_docker.sh", + "build_test_87_docker.sh", + "build_test_88_docker.sh", + "build_test_89_docker.sh", + "build_test_90_docker.sh", + "build_test_91_docker.sh", + "build_test_92_docker.sh", + "build_test_93_docker.sh", + "build_test_94_docker.sh", + "build_test_95_docker.sh", + "build_test_96_docker.sh", + "build_test_97_docker.sh", + "build_test_98_docker.sh", + "build_test_99_docker.sh", + "build_test_100_docker.sh", + "build_test_1_docker.sh", + "build_test_2_docker.sh", + "build_test_3_docker.sh", + "build_test_4_docker.sh", + "build_test_5_docker.sh", + "build_test_6_docker.sh", + "build_test_7_docker.sh", + "build_test_8_docker.sh", + "build_test_9_docker.sh", + "build_test_10_docker.sh", + "build_test_11_docker.sh", + "build_test_12_docker.sh", + "build_test_13_docker.sh", + "build_test_14_docker.sh", + "build_test_15_docker.sh", + "build_test_16_docker.sh", + "build_test_17_docker.sh", + "build_test_18_docker.sh", + "build_test_19_docker.sh", + "build_test_20_docker.sh", + "build_test_21_docker.sh", + "build_test_22_docker.sh", + "build_test_23_docker.sh", + "build_test_24_docker.sh", + "build_test_25_docker.sh", + "build_test_26_docker.sh", + "build_test_27_docker.sh", + "build_test_28_docker.sh", + "build_test_29_docker.sh", + "build_test_30_docker.sh", + "build_test_31_docker.sh", + "build_test_32_docker.sh", + "build_test_33_docker.sh", + "build_test_34_docker.sh", + "build_test_35_docker.sh", + "build_test_36_docker.sh", + "build_test_37_docker.sh", + "build_test_38_docker.sh", + "build_test_39_docker.sh", + "build_test_40_docker.sh", + "build_test_41_docker.sh", + "build_test_42_docker.sh", + "build_test_43_docker.sh", + "build_test_44_docker.sh", + "build_test_45_docker.sh", + "build_test_46_docker.sh", + "build_test_47_docker.sh", + "build_test_48_docker.sh", + "build_test_49_docker.sh", + "build_test_50_docker.sh", + "build_test_51_docker.sh", + "build_test_52_docker.sh", + "build_test_53_docker.sh", + "build_test_54_docker.sh", + "build_test_55_docker.sh", + "build_test_56_docker.sh", + "build_test_57_docker.sh", + "build_test_58_docker.sh", + "build_test_59_docker.sh", + "build_test_60_docker.sh", + "build_test_61_docker.sh", + "build_test_62_docker.sh", + "build_test_63_docker.sh", + "build_test_64_docker.sh", + "build_test_65_docker.sh", + "build_test_66_docker.sh", + "build_test_67_docker.sh", + "build_test_68_docker.sh", + "build_test_69_docker.sh", + "build_test_70_docker.sh", + "build_test_71_docker.sh", + "build_test_72_docker.sh", + "build_test_73_docker.sh", + "build_test_74_docker.sh", + "build_test_75_docker.sh", + "build_test_76_docker.sh", + "build_test_77_docker.sh", + "build_test_78_docker.sh", + "build_test_79_docker.sh", + "build_test_80_docker.sh", + "build_test_81_docker.sh", + "build_test_82_docker.sh", + "build_test_83_docker.sh", + "build_test_84_docker.sh", + "build_test_85_docker.sh", + "build_test_86_docker.sh", + "build_test_87_docker.sh", + "build_test_88_docker.sh", + "build_test_89_docker.sh", + "build_test_90_docker.sh", + "build_test_91_docker.sh", + "build_test_92_docker.sh", + "build_test_93_docker.sh", + "build_test_94_docker.sh", + "build_test_95_docker.sh", + "build_test_96_docker.sh", + "build_test_97_docker.sh", + "build_test_98_docker.sh", + "build_test_99_docker.sh", + "build_test_100_docker.sh", + "build_test_1_docker.sh", + "build_test_2_docker.sh", + "build_test_3_docker.sh", + "build_test_4_docker.sh", + "build_test_5_docker.sh", + "build_test_6_docker.sh", + "build_test_7_docker.sh", + "build_test_8_docker.sh", + "build_test_9_docker.sh", + "build_test_10_docker.sh", + "build_test_11_docker.sh", + "build_test_12_docker.sh", + "build_test_13_docker.sh", + "build_test_14_docker.sh", + "build_test_15_docker.sh", + "build_test_16_docker.sh", + "build_test_17_docker.sh", + "build_test_18_docker.sh", + "build_test_19_docker.sh", + "build_test_20_docker.sh", + "build_test_21_docker.sh", + "build_test_22_docker.sh", + "build_test_23_docker.sh", + "build_test_24_docker.sh", + "build_test_25_docker.sh", + "build_test_26_docker.sh", + "build_test_27_docker.sh", + "build_test_28_docker.sh", + "build_test_29_docker.sh", + "build_test_30_docker.sh", + "build_test_31_docker.sh", + "build_test_32_docker.sh", + "build_test_33_docker.sh", + "build_test_34_docker.sh", + "build_test_35_docker.sh", + "build_test_36_docker.sh", + "build_test_37_docker.sh", + "build_test_38_docker.sh", + "build_test_39_docker.sh", + "build_test_40_docker.sh", + "build_test_41_docker.sh", + "build_test_42_docker.sh", + "build_test_43_docker.sh", + "build_test_44_docker.sh", + "build_test_45_docker.sh", + "build_test_46_docker.sh", + "build_test_47_docker.sh", + "build_test_48_docker.sh", + "build_test_49_docker.sh", + "build_test_50_docker.sh", + "build_test_51_docker.sh", + "build_test_52_docker.sh", + "build_test_53_docker.sh", + "build_test_54_docker.sh", + "build_test_55_docker.sh", + "build_test_56_docker.sh", + "build_test_57_docker.sh", + "build_test_58_docker.sh", + "build_test_59_docker.sh", + "build_test_60_docker.sh", + "build_test_61_docker.sh", + "build_test_62_docker.sh", + "build_test_63_docker.sh", + "build_test_64_docker.sh", + "build_test_65_docker.sh", + "build_test_66_docker.sh", + "build_test_67_docker.sh", + "build_test_68_docker.sh", + "build_test_69_docker.sh", + "build_test_70_docker.sh", + "build_test_71_docker.sh", + "build_test_72_docker.sh", + "build_test_73_docker.sh", + "build_test_74_docker.sh", + "build_test_75_docker.sh", + "build_test_76_docker.sh", + "build_test_77_docker.sh", + "build_test_78_docker.sh", + "build_test_79_docker.sh", + "build_test_80_docker.sh", + "build_test_81_docker.sh", + "build_test_82_docker.sh", + "build_test_83_docker.sh", + "build_test_84_docker.sh", + "build_test_85_docker.sh", + "build_test_86_docker.sh", + "build_test_87_docker.sh", + "build_test_88_docker.sh", + "build_test_89_docker.sh", + "build_test_90_docker.sh", + "build_test_91_docker.sh", + "build_test_92_docker.sh", + "build_test_93_docker.sh", + "build_test_94_docker.sh", + "build_test_95_docker.sh", + "build_test_96_docker.sh", + "build_test_97_docker.sh", + "build_test_98_docker.sh", + "build_test_99_docker.sh", + "build_test_100_docker.sh" + ], + "failed_images": [ + "build_test_1_docker.sh", + "build_test_2_docker.sh", + "build_test_3_docker.sh", + "build_test_4_docker.sh", + "build_test_5_docker.sh", + "build_test_6_docker.sh", + "build_test_7_docker.sh", + "build_test_8_docker.sh", + "build_test_9_docker.sh", + "build_test_10_docker.sh", + "build_test_11_docker.sh", + "build_test_12_docker.sh", + "build_test_13_docker.sh", + "build_test_14_docker.sh", + "build_test_15_docker.sh", + "build_test_16_docker.sh", + "build_test_17_docker.sh", + "build_test_18_docker.sh", + "build_test_19_docker.sh", + "build_test_20_docker.sh", + "build_test_21_docker.sh", + "build_test_22_docker.sh", + "build_test_23_docker.sh", + "build_test_24_docker.sh", + "build_test_25_docker.sh", + "build_test_26_docker.sh", + "build_test_27_docker.sh", + "build_test_28_docker.sh", + "build_test_29_docker.sh", + "build_test_30_docker.sh", + "build_test_31_docker.sh", + "build_test_32_docker.sh", + "build_test_33_docker.sh", + "build_test_34_docker.sh", + "build_test_35_docker.sh", + "build_test_36_docker.sh", + "build_test_37_docker.sh", + "build_test_38_docker.sh", + "build_test_39_docker.sh", + "build_test_40_docker.sh", + "build_test_41_docker.sh", + "build_test_42_docker.sh", + "build_test_43_docker.sh", + "build_test_44_docker.sh", + "build_test_45_docker.sh", + "build_test_46_docker.sh", + "build_test_47_docker.sh", + "build_test_48_docker.sh", + "build_test_49_docker.sh", + "build_test_50_docker.sh", + "build_test_51_docker.sh", + "build_test_52_docker.sh", + "build_test_53_docker.sh", + "build_test_54_docker.sh", + "build_test_55_docker.sh", + "build_test_56_docker.sh", + "build_test_57_docker.sh", + "build_test_58_docker.sh", + "build_test_59_docker.sh", + "build_test_60_docker.sh", + "build_test_61_docker.sh", + "build_test_62_docker.sh", + "build_test_63_docker.sh", + "build_test_64_docker.sh", + "build_test_65_docker.sh", + "build_test_66_docker.sh", + "build_test_67_docker.sh", + "build_test_68_docker.sh", + "build_test_69_docker.sh", + "build_test_70_docker.sh", + "build_test_71_docker.sh", + "build_test_72_docker.sh", + "build_test_73_docker.sh", + "build_test_74_docker.sh", + "build_test_75_docker.sh", + "build_test_76_docker.sh", + "build_test_77_docker.sh", + "build_test_78_docker.sh", + "build_test_79_docker.sh", + "build_test_80_docker.sh", + "build_test_81_docker.sh", + "build_test_82_docker.sh", + "build_test_83_docker.sh", + "build_test_84_docker.sh", + "build_test_85_docker.sh", + "build_test_86_docker.sh", + "build_test_87_docker.sh", + "build_test_88_docker.sh", + "build_test_89_docker.sh", + "build_test_90_docker.sh", + "build_test_91_docker.sh", + "build_test_92_docker.sh", + "build_test_93_docker.sh", + "build_test_94_docker.sh", + "build_test_95_docker.sh", + "build_test_96_docker.sh", + "build_test_97_docker.sh", + "build_test_98_docker.sh", + "build_test_99_docker.sh", + "build_test_100_docker.sh" + ], + "error": "" +} \ No newline at end of file diff --git a/tests/test-error-docker-payload.json b/tests/test-error-docker-payload.json new file mode 100644 index 0000000..20597b6 --- /dev/null +++ b/tests/test-error-docker-payload.json @@ -0,0 +1,5 @@ +{ + "updated_images": [], + "failed_images": [], + "error": "JSONDecodeError('Expecting value: line 1 column 1 (char 0)')" +} \ No newline at end of file diff --git a/tests/test-error-mdxcanvas-payload.json b/tests/test-error-mdxcanvas-payload.json new file mode 100644 index 0000000..68762f7 --- /dev/null +++ b/tests/test-error-mdxcanvas-payload.json @@ -0,0 +1,17 @@ +{ + "deployed_content": [ + [ + "quiz_question_order", + "day-2-prep-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610843" + ] + ], + "content_to_review": [ + [ + "quiz_question_order", + "day-4-prep-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610844" + ] + ], + "error": "JSONDecodeError: Expecting value: line 1 column 1 (char 0)" +} \ No newline at end of file diff --git a/tests/test-large-mdxcanvas-payload.json b/tests/test-large-mdxcanvas-payload.json new file mode 100644 index 0000000..a2c248a --- /dev/null +++ b/tests/test-large-mdxcanvas-payload.json @@ -0,0 +1,6151 @@ +{ + "deployed_content": [ + [ + "module", + "unit-1", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "unit-2", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "unit-3", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "unit-4", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "comprehensive-review", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-01", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-02", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-03", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-04", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-05", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-06", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-07", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-08", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-09", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-10", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-11", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-12", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-13", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-14", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-15", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-16", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-17", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-18", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-19", + "https://byu.instructure.com/courses/20736" + ], + [ + "module", + "load-test-module-20", + "https://byu.instructure.com/courses/20736" + ], + [ + "module_item", + "load-test-module-20|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "module_item", + "load-test-module-14|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "page", + "load-test-page-161", + "https://byu.instructure.com/courses/20736/pages/load-test-page-161-coastal-resilience-2" + ], + [ + "page", + "load-test-page-131", + "https://byu.instructure.com/courses/20736/pages/load-test-page-131-current-driven-dispersal-2" + ], + [ + "page", + "load-test-page-191", + "https://byu.instructure.com/courses/20736/pages/load-test-page-191-current-driven-dispersal-2" + ], + [ + "module_item", + "load-test-module-17|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "page", + "load-test-page-181", + "https://byu.instructure.com/courses/20736/pages/load-test-page-181-predator-prey-balance-2" + ], + [ + "page", + "load-test-page-151", + "https://byu.instructure.com/courses/20736/pages/load-test-page-151-food-web-transfer-2" + ], + [ + "page", + "load-test-page-171", + "https://byu.instructure.com/courses/20736/pages/load-test-page-171-coastal-resilience-2" + ], + [ + "module_item", + "load-test-module-18|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "module_item", + "load-test-module-15|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "module_item", + "load-test-module-19|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "page", + "load-test-page-141", + "https://byu.instructure.com/courses/20736/pages/load-test-page-141-food-web-transfer-2" + ], + [ + "module_item", + "load-test-module-16|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "page", + "load-test-page-111", + "https://byu.instructure.com/courses/20736/pages/load-test-page-111-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-13|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "page", + "load-test-page-121", + "https://byu.instructure.com/courses/20736/pages/load-test-page-121-food-web-transfer-2" + ], + [ + "module_item", + "load-test-module-12|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "page", + "load-test-page-091", + "https://byu.instructure.com/courses/20736/pages/load-test-page-091-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-11|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "page", + "load-test-page-101", + "https://byu.instructure.com/courses/20736/pages/load-test-page-101-marine-conservation-policy-2" + ], + [ + "page", + "load-test-page-081", + "https://byu.instructure.com/courses/20736/pages/load-test-page-081-reef-restoration-2" + ], + [ + "module_item", + "load-test-module-09|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "module_item", + "load-test-module-10|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "page", + "load-test-page-071", + "https://byu.instructure.com/courses/20736/pages/load-test-page-071-food-web-transfer-2" + ], + [ + "module_item", + "load-test-module-08|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "page", + "load-test-page-061", + "https://byu.instructure.com/courses/20736/pages/load-test-page-061-reef-restoration-2" + ], + [ + "module_item", + "load-test-module-06|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "page", + "load-test-page-041", + "https://byu.instructure.com/courses/20736/pages/load-test-page-041-current-driven-dispersal-2" + ], + [ + "module_item", + "load-test-module-07|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "module_item", + "load-test-module-05|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "page", + "load-test-page-051", + "https://byu.instructure.com/courses/20736/pages/load-test-page-051-predator-prey-balance-2" + ], + [ + "page", + "load-test-page-011", + "https://byu.instructure.com/courses/20736/pages/load-test-page-011-reef-restoration-2" + ], + [ + "page", + "load-test-page-021", + "https://byu.instructure.com/courses/20736/pages/load-test-page-021-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-04|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "page", + "load-test-page-031", + "https://byu.instructure.com/courses/20736/pages/load-test-page-031-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-02|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "module_item", + "load-test-module-03|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "page", + "load-test-page-192", + "https://byu.instructure.com/courses/20736/pages/load-test-page-192-nutrient-cycling-2" + ], + [ + "page", + "load-test-page-182", + "https://byu.instructure.com/courses/20736/pages/load-test-page-182-benthic-biodiversity-2" + ], + [ + "module_item", + "load-test-module-20|load-test-page-191", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "module_item", + "load-test-module-19|load-test-page-181", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "page", + "load-test-page-172", + "https://byu.instructure.com/courses/20736/pages/load-test-page-172-migration-behavior-2" + ], + [ + "module_item", + "load-test-module-01|Synthetic Pages", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "page", + "load-test-page-001", + "https://byu.instructure.com/courses/20736/pages/load-test-page-001-marine-conservation-policy-2" + ], + [ + "page", + "load-test-page-162", + "https://byu.instructure.com/courses/20736/pages/load-test-page-162-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-18|load-test-page-171", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "page", + "load-test-page-152", + "https://byu.instructure.com/courses/20736/pages/load-test-page-152-predator-prey-balance-2" + ], + [ + "module_item", + "load-test-module-17|load-test-page-161", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "module_item", + "load-test-module-16|load-test-page-151", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "page", + "load-test-page-142", + "https://byu.instructure.com/courses/20736/pages/load-test-page-142-coastal-resilience-2" + ], + [ + "page", + "load-test-page-132", + "https://byu.instructure.com/courses/20736/pages/load-test-page-132-benthic-biodiversity-2" + ], + [ + "page", + "load-test-page-122", + "https://byu.instructure.com/courses/20736/pages/load-test-page-122-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-15|load-test-page-141", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "module_item", + "load-test-module-13|load-test-page-121", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "page", + "load-test-page-092", + "https://byu.instructure.com/courses/20736/pages/load-test-page-092-marine-conservation-policy-2" + ], + [ + "page", + "load-test-page-102", + "https://byu.instructure.com/courses/20736/pages/load-test-page-102-predator-prey-balance-2" + ], + [ + "module_item", + "load-test-module-14|load-test-page-131", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "module_item", + "load-test-module-10|load-test-page-091", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "page", + "load-test-page-112", + "https://byu.instructure.com/courses/20736/pages/load-test-page-112-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-12|load-test-page-111", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "module_item", + "load-test-module-11|load-test-page-101", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "page", + "load-test-page-082", + "https://byu.instructure.com/courses/20736/pages/load-test-page-082-predator-prey-balance-2" + ], + [ + "page", + "load-test-page-072", + "https://byu.instructure.com/courses/20736/pages/load-test-page-072-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-08|load-test-page-071", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "page", + "load-test-page-062", + "https://byu.instructure.com/courses/20736/pages/load-test-page-062-benthic-biodiversity-2" + ], + [ + "page", + "load-test-page-052", + "https://byu.instructure.com/courses/20736/pages/load-test-page-052-marine-conservation-policy-2" + ], + [ + "module_item", + "load-test-module-09|load-test-page-081", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "page", + "load-test-page-042", + "https://byu.instructure.com/courses/20736/pages/load-test-page-042-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-06|load-test-page-051", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "page", + "load-test-page-022", + "https://byu.instructure.com/courses/20736/pages/load-test-page-022-coastal-resilience-2" + ], + [ + "module_item", + "load-test-module-05|load-test-page-041", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "page", + "load-test-page-032", + "https://byu.instructure.com/courses/20736/pages/load-test-page-032-reef-restoration-2" + ], + [ + "module_item", + "load-test-module-07|load-test-page-061", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "module_item", + "load-test-module-04|load-test-page-031", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "module_item", + "load-test-module-03|load-test-page-021", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "page", + "load-test-page-012", + "https://byu.instructure.com/courses/20736/pages/load-test-page-012-reef-restoration-2" + ], + [ + "page", + "load-test-page-002", + "https://byu.instructure.com/courses/20736/pages/load-test-page-002-nutrient-cycling-2" + ], + [ + "page", + "load-test-page-193", + "https://byu.instructure.com/courses/20736/pages/load-test-page-193-migration-behavior-2" + ], + [ + "module_item", + "load-test-module-01|load-test-page-001", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "module_item", + "load-test-module-20|load-test-page-192", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "module_item", + "load-test-module-02|load-test-page-011", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "page", + "load-test-page-163", + "https://byu.instructure.com/courses/20736/pages/load-test-page-163-deep-sea-adaptation-2" + ], + [ + "page", + "load-test-page-183", + "https://byu.instructure.com/courses/20736/pages/load-test-page-183-ocean-chemistry-2" + ], + [ + "module_item", + "load-test-module-18|load-test-page-172", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "page", + "load-test-page-173", + "https://byu.instructure.com/courses/20736/pages/load-test-page-173-migration-behavior-2" + ], + [ + "module_item", + "load-test-module-17|load-test-page-162", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "page", + "load-test-page-153", + "https://byu.instructure.com/courses/20736/pages/load-test-page-153-benthic-biodiversity-2" + ], + [ + "module_item", + "load-test-module-16|load-test-page-152", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "page", + "load-test-page-133", + "https://byu.instructure.com/courses/20736/pages/load-test-page-133-current-driven-dispersal-2" + ], + [ + "module_item", + "load-test-module-19|load-test-page-182", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "page", + "load-test-page-143", + "https://byu.instructure.com/courses/20736/pages/load-test-page-143-deep-sea-adaptation-2" + ], + [ + "module_item", + "load-test-module-14|load-test-page-132", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "module_item", + "load-test-module-15|load-test-page-142", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "page", + "load-test-page-123", + "https://byu.instructure.com/courses/20736/pages/load-test-page-123-current-driven-dispersal-2" + ], + [ + "page", + "load-test-page-113", + "https://byu.instructure.com/courses/20736/pages/load-test-page-113-migration-behavior-2" + ], + [ + "module_item", + "load-test-module-13|load-test-page-122", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "page", + "load-test-page-103", + "https://byu.instructure.com/courses/20736/pages/load-test-page-103-plankton-dynamics-2" + ], + [ + "page", + "load-test-page-093", + "https://byu.instructure.com/courses/20736/pages/load-test-page-093-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-09|load-test-page-082", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "page", + "load-test-page-083", + "https://byu.instructure.com/courses/20736/pages/load-test-page-083-deep-sea-adaptation-2" + ], + [ + "module_item", + "load-test-module-11|load-test-page-102", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "module_item", + "load-test-module-12|load-test-page-112", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "page", + "load-test-page-073", + "https://byu.instructure.com/courses/20736/pages/load-test-page-073-migration-behavior-2" + ], + [ + "module_item", + "load-test-module-10|load-test-page-092", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "module_item", + "load-test-module-08|load-test-page-072", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "page", + "load-test-page-063", + "https://byu.instructure.com/courses/20736/pages/load-test-page-063-ocean-chemistry-2" + ], + [ + "page", + "load-test-page-053", + "https://byu.instructure.com/courses/20736/pages/load-test-page-053-reef-restoration-2" + ], + [ + "module_item", + "load-test-module-07|load-test-page-062", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "page", + "load-test-page-043", + "https://byu.instructure.com/courses/20736/pages/load-test-page-043-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-06|load-test-page-052", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "page", + "load-test-page-033", + "https://byu.instructure.com/courses/20736/pages/load-test-page-033-deep-sea-adaptation-2" + ], + [ + "page", + "load-test-page-013", + "https://byu.instructure.com/courses/20736/pages/load-test-page-013-ocean-chemistry-2" + ], + [ + "module_item", + "load-test-module-05|load-test-page-042", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "page", + "load-test-page-023", + "https://byu.instructure.com/courses/20736/pages/load-test-page-023-ocean-chemistry-2" + ], + [ + "module_item", + "load-test-module-04|load-test-page-032", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "module_item", + "load-test-module-03|load-test-page-022", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "page", + "load-test-page-194", + "https://byu.instructure.com/courses/20736/pages/load-test-page-194-food-web-transfer-2" + ], + [ + "module_item", + "load-test-module-02|load-test-page-012", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "module_item", + "load-test-module-19|load-test-page-183", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "page", + "load-test-page-184", + "https://byu.instructure.com/courses/20736/pages/load-test-page-184-ocean-chemistry-2" + ], + [ + "page", + "load-test-page-003", + "https://byu.instructure.com/courses/20736/pages/load-test-page-003-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-01|load-test-page-002", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "module_item", + "load-test-module-20|load-test-page-193", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "page", + "load-test-page-174", + "https://byu.instructure.com/courses/20736/pages/load-test-page-174-reef-restoration-2" + ], + [ + "page", + "load-test-page-154", + "https://byu.instructure.com/courses/20736/pages/load-test-page-154-deep-sea-adaptation-2" + ], + [ + "module_item", + "load-test-module-18|load-test-page-173", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "page", + "load-test-page-164", + "https://byu.instructure.com/courses/20736/pages/load-test-page-164-reef-restoration-2" + ], + [ + "module_item", + "load-test-module-17|load-test-page-163", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "module_item", + "load-test-module-16|load-test-page-153", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "page", + "load-test-page-144", + "https://byu.instructure.com/courses/20736/pages/load-test-page-144-marine-conservation-policy-2" + ], + [ + "page", + "load-test-page-134", + "https://byu.instructure.com/courses/20736/pages/load-test-page-134-ocean-chemistry-2" + ], + [ + "page", + "load-test-page-114", + "https://byu.instructure.com/courses/20736/pages/load-test-page-114-deep-sea-adaptation-2" + ], + [ + "page", + "load-test-page-124", + "https://byu.instructure.com/courses/20736/pages/load-test-page-124-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-15|load-test-page-143", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "page", + "load-test-page-104", + "https://byu.instructure.com/courses/20736/pages/load-test-page-104-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-13|load-test-page-123", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "module_item", + "load-test-module-14|load-test-page-133", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "module_item", + "load-test-module-12|load-test-page-113", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "module_item", + "load-test-module-11|load-test-page-103", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "page", + "load-test-page-084", + "https://byu.instructure.com/courses/20736/pages/load-test-page-084-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-10|load-test-page-093", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "module_item", + "load-test-module-09|load-test-page-083", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "page", + "load-test-page-094", + "https://byu.instructure.com/courses/20736/pages/load-test-page-094-deep-sea-adaptation-2" + ], + [ + "page", + "load-test-page-074", + "https://byu.instructure.com/courses/20736/pages/load-test-page-074-ocean-chemistry-2" + ], + [ + "page", + "load-test-page-054", + "https://byu.instructure.com/courses/20736/pages/load-test-page-054-current-driven-dispersal-2" + ], + [ + "page", + "load-test-page-064", + "https://byu.instructure.com/courses/20736/pages/load-test-page-064-migration-behavior-2" + ], + [ + "page", + "load-test-page-044", + "https://byu.instructure.com/courses/20736/pages/load-test-page-044-predator-prey-balance-2" + ], + [ + "module_item", + "load-test-module-07|load-test-page-063", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "page", + "load-test-page-034", + "https://byu.instructure.com/courses/20736/pages/load-test-page-034-migration-behavior-2" + ], + [ + "module_item", + "load-test-module-05|load-test-page-043", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "module_item", + "load-test-module-06|load-test-page-053", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "module_item", + "load-test-module-08|load-test-page-073", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "page", + "load-test-page-024", + "https://byu.instructure.com/courses/20736/pages/load-test-page-024-food-web-transfer-2" + ], + [ + "module_item", + "load-test-module-04|load-test-page-033", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "module_item", + "load-test-module-03|load-test-page-023", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "page", + "load-test-page-014", + "https://byu.instructure.com/courses/20736/pages/load-test-page-014-ocean-chemistry-2" + ], + [ + "page", + "load-test-page-004", + "https://byu.instructure.com/courses/20736/pages/load-test-page-004-ocean-chemistry-2" + ], + [ + "module_item", + "load-test-module-02|load-test-page-013", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "module_item", + "load-test-module-01|load-test-page-003", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "page", + "load-test-page-195", + "https://byu.instructure.com/courses/20736/pages/load-test-page-195-benthic-biodiversity-2" + ], + [ + "page", + "load-test-page-175", + "https://byu.instructure.com/courses/20736/pages/load-test-page-175-benthic-biodiversity-2" + ], + [ + "module_item", + "load-test-module-19|load-test-page-184", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "page", + "load-test-page-165", + "https://byu.instructure.com/courses/20736/pages/load-test-page-165-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-20|load-test-page-194", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "page", + "load-test-page-155", + "https://byu.instructure.com/courses/20736/pages/load-test-page-155-food-web-transfer-2" + ], + [ + "module_item", + "load-test-module-18|load-test-page-174", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "module_item", + "load-test-module-17|load-test-page-164", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "page", + "load-test-page-185", + "https://byu.instructure.com/courses/20736/pages/load-test-page-185-migration-behavior-2" + ], + [ + "page", + "load-test-page-145", + "https://byu.instructure.com/courses/20736/pages/load-test-page-145-ocean-chemistry-2" + ], + [ + "page", + "load-test-page-125", + "https://byu.instructure.com/courses/20736/pages/load-test-page-125-ocean-chemistry-2" + ], + [ + "module_item", + "load-test-module-16|load-test-page-154", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "page", + "load-test-page-135", + "https://byu.instructure.com/courses/20736/pages/load-test-page-135-migration-behavior-2" + ], + [ + "module_item", + "load-test-module-15|load-test-page-144", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "page", + "load-test-page-105", + "https://byu.instructure.com/courses/20736/pages/load-test-page-105-deep-sea-adaptation-2" + ], + [ + "module_item", + "load-test-module-14|load-test-page-134", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "page", + "load-test-page-115", + "https://byu.instructure.com/courses/20736/pages/load-test-page-115-reef-restoration-2" + ], + [ + "module_item", + "load-test-module-13|load-test-page-124", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "module_item", + "load-test-module-12|load-test-page-114", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "module_item", + "load-test-module-11|load-test-page-104", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "page", + "load-test-page-095", + "https://byu.instructure.com/courses/20736/pages/load-test-page-095-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-10|load-test-page-094", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "page", + "load-test-page-085", + "https://byu.instructure.com/courses/20736/pages/load-test-page-085-coastal-resilience-2" + ], + [ + "page", + "load-test-page-075", + "https://byu.instructure.com/courses/20736/pages/load-test-page-075-migration-behavior-2" + ], + [ + "page", + "load-test-page-065", + "https://byu.instructure.com/courses/20736/pages/load-test-page-065-marine-conservation-policy-2" + ], + [ + "module_item", + "load-test-module-09|load-test-page-084", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "page", + "load-test-page-055", + "https://byu.instructure.com/courses/20736/pages/load-test-page-055-marine-conservation-policy-2" + ], + [ + "page", + "load-test-page-045", + "https://byu.instructure.com/courses/20736/pages/load-test-page-045-ocean-chemistry-2" + ], + [ + "module_item", + "load-test-module-08|load-test-page-074", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "module_item", + "load-test-module-07|load-test-page-064", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "page", + "load-test-page-035", + "https://byu.instructure.com/courses/20736/pages/load-test-page-035-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-05|load-test-page-044", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "page", + "load-test-page-025", + "https://byu.instructure.com/courses/20736/pages/load-test-page-025-marine-conservation-policy-2" + ], + [ + "module_item", + "load-test-module-06|load-test-page-054", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "module_item", + "load-test-module-04|load-test-page-034", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "page", + "load-test-page-015", + "https://byu.instructure.com/courses/20736/pages/load-test-page-015-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-02|load-test-page-014", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "page", + "load-test-page-196", + "https://byu.instructure.com/courses/20736/pages/load-test-page-196-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-19|load-test-page-185", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "module_item", + "load-test-module-20|load-test-page-195", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "page", + "load-test-page-186", + "https://byu.instructure.com/courses/20736/pages/load-test-page-186-ocean-chemistry-2" + ], + [ + "page", + "load-test-page-005", + "https://byu.instructure.com/courses/20736/pages/load-test-page-005-plankton-dynamics-2" + ], + [ + "page", + "load-test-page-166", + "https://byu.instructure.com/courses/20736/pages/load-test-page-166-coastal-resilience-2" + ], + [ + "module_item", + "load-test-module-01|load-test-page-004", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "page", + "load-test-page-176", + "https://byu.instructure.com/courses/20736/pages/load-test-page-176-ocean-chemistry-2" + ], + [ + "module_item", + "load-test-module-03|load-test-page-024", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "module_item", + "load-test-module-18|load-test-page-175", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "module_item", + "load-test-module-17|load-test-page-165", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "module_item", + "load-test-module-16|load-test-page-155", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "page", + "load-test-page-156", + "https://byu.instructure.com/courses/20736/pages/load-test-page-156-predator-prey-balance-2" + ], + [ + "page", + "load-test-page-136", + "https://byu.instructure.com/courses/20736/pages/load-test-page-136-food-web-transfer-2" + ], + [ + "module_item", + "load-test-module-15|load-test-page-145", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "page", + "load-test-page-126", + "https://byu.instructure.com/courses/20736/pages/load-test-page-126-food-web-transfer-2" + ], + [ + "module_item", + "load-test-module-14|load-test-page-135", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "page", + "load-test-page-106", + "https://byu.instructure.com/courses/20736/pages/load-test-page-106-deep-sea-adaptation-2" + ], + [ + "module_item", + "load-test-module-12|load-test-page-115", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "page", + "load-test-page-096", + "https://byu.instructure.com/courses/20736/pages/load-test-page-096-food-web-transfer-2" + ], + [ + "page", + "load-test-page-116", + "https://byu.instructure.com/courses/20736/pages/load-test-page-116-deep-sea-adaptation-2" + ], + [ + "module_item", + "load-test-module-11|load-test-page-105", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "module_item", + "load-test-module-13|load-test-page-125", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "module_item", + "load-test-module-10|load-test-page-095", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "module_item", + "load-test-module-09|load-test-page-085", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "page", + "load-test-page-086", + "https://byu.instructure.com/courses/20736/pages/load-test-page-086-marine-conservation-policy-2" + ], + [ + "page", + "load-test-page-056", + "https://byu.instructure.com/courses/20736/pages/load-test-page-056-benthic-biodiversity-2" + ], + [ + "page", + "load-test-page-076", + "https://byu.instructure.com/courses/20736/pages/load-test-page-076-benthic-biodiversity-2" + ], + [ + "page", + "load-test-page-046", + "https://byu.instructure.com/courses/20736/pages/load-test-page-046-nutrient-cycling-2" + ], + [ + "page", + "load-test-page-066", + "https://byu.instructure.com/courses/20736/pages/load-test-page-066-benthic-biodiversity-2" + ], + [ + "module_item", + "load-test-module-04|load-test-page-035", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "page", + "load-test-page-026", + "https://byu.instructure.com/courses/20736/pages/load-test-page-026-migration-behavior-2" + ], + [ + "module_item", + "load-test-module-07|load-test-page-065", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "page", + "load-test-page-036", + "https://byu.instructure.com/courses/20736/pages/load-test-page-036-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-08|load-test-page-075", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "module_item", + "load-test-module-06|load-test-page-055", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "module_item", + "load-test-module-05|load-test-page-045", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "module_item", + "load-test-module-03|load-test-page-025", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "page", + "load-test-page-146", + "https://byu.instructure.com/courses/20736/pages/load-test-page-146-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-02|load-test-page-015", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "page", + "load-test-page-016", + "https://byu.instructure.com/courses/20736/pages/load-test-page-016-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-01|load-test-page-005", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "page", + "load-test-page-197", + "https://byu.instructure.com/courses/20736/pages/load-test-page-197-predator-prey-balance-2" + ], + [ + "page", + "load-test-page-006", + "https://byu.instructure.com/courses/20736/pages/load-test-page-006-nutrient-cycling-2" + ], + [ + "page", + "load-test-page-177", + "https://byu.instructure.com/courses/20736/pages/load-test-page-177-predator-prey-balance-2" + ], + [ + "module_item", + "load-test-module-20|load-test-page-196", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "module_item", + "load-test-module-19|load-test-page-186", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "page", + "load-test-page-187", + "https://byu.instructure.com/courses/20736/pages/load-test-page-187-current-driven-dispersal-2" + ], + [ + "module_item", + "load-test-module-18|load-test-page-176", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "page", + "load-test-page-167", + "https://byu.instructure.com/courses/20736/pages/load-test-page-167-food-web-transfer-2" + ], + [ + "module_item", + "load-test-module-16|load-test-page-156", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "page", + "load-test-page-157", + "https://byu.instructure.com/courses/20736/pages/load-test-page-157-migration-behavior-2" + ], + [ + "module_item", + "load-test-module-17|load-test-page-166", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "page", + "load-test-page-147", + "https://byu.instructure.com/courses/20736/pages/load-test-page-147-ocean-chemistry-2" + ], + [ + "page", + "load-test-page-137", + "https://byu.instructure.com/courses/20736/pages/load-test-page-137-current-driven-dispersal-2" + ], + [ + "module_item", + "load-test-module-15|load-test-page-146", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "page", + "load-test-page-127", + "https://byu.instructure.com/courses/20736/pages/load-test-page-127-current-driven-dispersal-2" + ], + [ + "module_item", + "load-test-module-14|load-test-page-136", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "page", + "load-test-page-117", + "https://byu.instructure.com/courses/20736/pages/load-test-page-117-deep-sea-adaptation-2" + ], + [ + "page", + "load-test-page-107", + "https://byu.instructure.com/courses/20736/pages/load-test-page-107-benthic-biodiversity-2" + ], + [ + "module_item", + "load-test-module-13|load-test-page-126", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "page", + "load-test-page-097", + "https://byu.instructure.com/courses/20736/pages/load-test-page-097-food-web-transfer-2" + ], + [ + "module_item", + "load-test-module-11|load-test-page-106", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "module_item", + "load-test-module-10|load-test-page-096", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "page", + "load-test-page-077", + "https://byu.instructure.com/courses/20736/pages/load-test-page-077-marine-conservation-policy-2" + ], + [ + "module_item", + "load-test-module-09|load-test-page-086", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "page", + "load-test-page-087", + "https://byu.instructure.com/courses/20736/pages/load-test-page-087-current-driven-dispersal-2" + ], + [ + "page", + "load-test-page-067", + "https://byu.instructure.com/courses/20736/pages/load-test-page-067-marine-conservation-policy-2" + ], + [ + "module_item", + "load-test-module-08|load-test-page-076", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "page", + "load-test-page-057", + "https://byu.instructure.com/courses/20736/pages/load-test-page-057-benthic-biodiversity-2" + ], + [ + "module_item", + "load-test-module-06|load-test-page-056", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "module_item", + "load-test-module-12|load-test-page-116", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "page", + "load-test-page-047", + "https://byu.instructure.com/courses/20736/pages/load-test-page-047-benthic-biodiversity-2" + ], + [ + "module_item", + "load-test-module-05|load-test-page-046", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "module_item", + "load-test-module-07|load-test-page-066", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "page", + "load-test-page-037", + "https://byu.instructure.com/courses/20736/pages/load-test-page-037-deep-sea-adaptation-2" + ], + [ + "module_item", + "load-test-module-04|load-test-page-036", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "page", + "load-test-page-027", + "https://byu.instructure.com/courses/20736/pages/load-test-page-027-current-driven-dispersal-2" + ], + [ + "page", + "load-test-page-017", + "https://byu.instructure.com/courses/20736/pages/load-test-page-017-marine-conservation-policy-2" + ], + [ + "module_item", + "load-test-module-03|load-test-page-026", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "page", + "load-test-page-007", + "https://byu.instructure.com/courses/20736/pages/load-test-page-007-ocean-chemistry-2" + ], + [ + "page", + "load-test-page-178", + "https://byu.instructure.com/courses/20736/pages/load-test-page-178-reef-restoration-2" + ], + [ + "module_item", + "load-test-module-01|load-test-page-006", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "module_item", + "load-test-module-02|load-test-page-016", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "page", + "load-test-page-198", + "https://byu.instructure.com/courses/20736/pages/load-test-page-198-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-20|load-test-page-197", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "page", + "load-test-page-188", + "https://byu.instructure.com/courses/20736/pages/load-test-page-188-reef-restoration-2" + ], + [ + "module_item", + "load-test-module-19|load-test-page-187", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "module_item", + "load-test-module-18|load-test-page-177", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "page", + "load-test-page-168", + "https://byu.instructure.com/courses/20736/pages/load-test-page-168-ocean-chemistry-2" + ], + [ + "module_item", + "load-test-module-17|load-test-page-167", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "page", + "load-test-page-158", + "https://byu.instructure.com/courses/20736/pages/load-test-page-158-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-16|load-test-page-157", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "module_item", + "load-test-module-15|load-test-page-147", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "page", + "load-test-page-148", + "https://byu.instructure.com/courses/20736/pages/load-test-page-148-nutrient-cycling-2" + ], + [ + "page", + "load-test-page-138", + "https://byu.instructure.com/courses/20736/pages/load-test-page-138-deep-sea-adaptation-2" + ], + [ + "page", + "load-test-page-128", + "https://byu.instructure.com/courses/20736/pages/load-test-page-128-deep-sea-adaptation-2" + ], + [ + "module_item", + "load-test-module-14|load-test-page-137", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "module_item", + "load-test-module-13|load-test-page-127", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "page", + "load-test-page-118", + "https://byu.instructure.com/courses/20736/pages/load-test-page-118-benthic-biodiversity-2" + ], + [ + "page", + "load-test-page-108", + "https://byu.instructure.com/courses/20736/pages/load-test-page-108-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-11|load-test-page-107", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "module_item", + "load-test-module-12|load-test-page-117", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "page", + "load-test-page-098", + "https://byu.instructure.com/courses/20736/pages/load-test-page-098-food-web-transfer-2" + ], + [ + "page", + "load-test-page-088", + "https://byu.instructure.com/courses/20736/pages/load-test-page-088-deep-sea-adaptation-2" + ], + [ + "page", + "load-test-page-078", + "https://byu.instructure.com/courses/20736/pages/load-test-page-078-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-10|load-test-page-097", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "page", + "load-test-page-068", + "https://byu.instructure.com/courses/20736/pages/load-test-page-068-reef-restoration-2" + ], + [ + "module_item", + "load-test-module-09|load-test-page-087", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "page", + "load-test-page-058", + "https://byu.instructure.com/courses/20736/pages/load-test-page-058-benthic-biodiversity-2" + ], + [ + "module_item", + "load-test-module-07|load-test-page-067", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "module_item", + "load-test-module-08|load-test-page-077", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "page", + "load-test-page-048", + "https://byu.instructure.com/courses/20736/pages/load-test-page-048-migration-behavior-2" + ], + [ + "page", + "load-test-page-038", + "https://byu.instructure.com/courses/20736/pages/load-test-page-038-ocean-chemistry-2" + ], + [ + "module_item", + "load-test-module-06|load-test-page-057", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "page", + "load-test-page-028", + "https://byu.instructure.com/courses/20736/pages/load-test-page-028-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-04|load-test-page-037", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "page", + "load-test-page-018", + "https://byu.instructure.com/courses/20736/pages/load-test-page-018-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-03|load-test-page-027", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "module_item", + "load-test-module-05|load-test-page-047", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "module_item", + "load-test-module-02|load-test-page-017", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "module_item", + "load-test-module-01|load-test-page-007", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "page", + "load-test-page-008", + "https://byu.instructure.com/courses/20736/pages/load-test-page-008-food-web-transfer-2" + ], + [ + "page", + "load-test-page-199", + "https://byu.instructure.com/courses/20736/pages/load-test-page-199-plankton-dynamics-2" + ], + [ + "page", + "load-test-page-189", + "https://byu.instructure.com/courses/20736/pages/load-test-page-189-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-20|load-test-page-198", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "page", + "load-test-page-169", + "https://byu.instructure.com/courses/20736/pages/load-test-page-169-ocean-chemistry-2" + ], + [ + "page", + "load-test-page-179", + "https://byu.instructure.com/courses/20736/pages/load-test-page-179-deep-sea-adaptation-2" + ], + [ + "module_item", + "load-test-module-19|load-test-page-188", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "module_item", + "load-test-module-17|load-test-page-168", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "page", + "load-test-page-159", + "https://byu.instructure.com/courses/20736/pages/load-test-page-159-ocean-chemistry-2" + ], + [ + "page", + "load-test-page-149", + "https://byu.instructure.com/courses/20736/pages/load-test-page-149-reef-restoration-2" + ], + [ + "module_item", + "load-test-module-16|load-test-page-158", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "module_item", + "load-test-module-18|load-test-page-178", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "page", + "load-test-page-139", + "https://byu.instructure.com/courses/20736/pages/load-test-page-139-nutrient-cycling-2" + ], + [ + "page", + "load-test-page-129", + "https://byu.instructure.com/courses/20736/pages/load-test-page-129-benthic-biodiversity-2" + ], + [ + "page", + "load-test-page-119", + "https://byu.instructure.com/courses/20736/pages/load-test-page-119-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-14|load-test-page-138", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "module_item", + "load-test-module-13|load-test-page-128", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "page", + "load-test-page-109", + "https://byu.instructure.com/courses/20736/pages/load-test-page-109-reef-restoration-2" + ], + [ + "module_item", + "load-test-module-15|load-test-page-148", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "module_item", + "load-test-module-12|load-test-page-118", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "page", + "load-test-page-099", + "https://byu.instructure.com/courses/20736/pages/load-test-page-099-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-11|load-test-page-108", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "module_item", + "load-test-module-10|load-test-page-098", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "page", + "load-test-page-089", + "https://byu.instructure.com/courses/20736/pages/load-test-page-089-predator-prey-balance-2" + ], + [ + "page", + "load-test-page-079", + "https://byu.instructure.com/courses/20736/pages/load-test-page-079-migration-behavior-2" + ], + [ + "page", + "load-test-page-059", + "https://byu.instructure.com/courses/20736/pages/load-test-page-059-predator-prey-balance-2" + ], + [ + "module_item", + "load-test-module-09|load-test-page-088", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "module_item", + "load-test-module-08|load-test-page-078", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "page", + "load-test-page-069", + "https://byu.instructure.com/courses/20736/pages/load-test-page-069-marine-conservation-policy-2" + ], + [ + "page", + "load-test-page-049", + "https://byu.instructure.com/courses/20736/pages/load-test-page-049-plankton-dynamics-2" + ], + [ + "module_item", + "load-test-module-06|load-test-page-058", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "module_item", + "load-test-module-07|load-test-page-068", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "page", + "load-test-page-039", + "https://byu.instructure.com/courses/20736/pages/load-test-page-039-current-driven-dispersal-2" + ], + [ + "module_item", + "load-test-module-05|load-test-page-048", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "page", + "load-test-page-029", + "https://byu.instructure.com/courses/20736/pages/load-test-page-029-deep-sea-adaptation-2" + ], + [ + "module_item", + "load-test-module-04|load-test-page-038", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "page", + "load-test-page-019", + "https://byu.instructure.com/courses/20736/pages/load-test-page-019-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-03|load-test-page-028", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "page", + "load-test-page-009", + "https://byu.instructure.com/courses/20736/pages/load-test-page-009-nutrient-cycling-2" + ], + [ + "page", + "load-test-page-200", + "https://byu.instructure.com/courses/20736/pages/load-test-page-200-ocean-chemistry-2" + ], + [ + "page", + "load-test-page-190", + "https://byu.instructure.com/courses/20736/pages/load-test-page-190-coastal-resilience-2" + ], + [ + "module_item", + "load-test-module-01|load-test-page-008", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "module_item", + "load-test-module-02|load-test-page-018", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "module_item", + "load-test-module-20|load-test-page-199", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "page", + "load-test-page-180", + "https://byu.instructure.com/courses/20736/pages/load-test-page-180-coastal-resilience-2" + ], + [ + "page", + "load-test-page-170", + "https://byu.instructure.com/courses/20736/pages/load-test-page-170-food-web-transfer-2" + ], + [ + "module_item", + "load-test-module-19|load-test-page-189", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "page", + "load-test-page-160", + "https://byu.instructure.com/courses/20736/pages/load-test-page-160-migration-behavior-2" + ], + [ + "module_item", + "load-test-module-17|load-test-page-169", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "module_item", + "load-test-module-18|load-test-page-179", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "page", + "load-test-page-150", + "https://byu.instructure.com/courses/20736/pages/load-test-page-150-migration-behavior-2" + ], + [ + "module_item", + "load-test-module-16|load-test-page-159", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "page", + "load-test-page-140", + "https://byu.instructure.com/courses/20736/pages/load-test-page-140-marine-conservation-policy-2" + ], + [ + "module_item", + "load-test-module-15|load-test-page-149", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "module_item", + "load-test-module-14|load-test-page-139", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "page", + "load-test-page-130", + "https://byu.instructure.com/courses/20736/pages/load-test-page-130-predator-prey-balance-2" + ], + [ + "module_item", + "load-test-module-13|load-test-page-129", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "page", + "load-test-page-120", + "https://byu.instructure.com/courses/20736/pages/load-test-page-120-marine-conservation-policy-2" + ], + [ + "page", + "load-test-page-110", + "https://byu.instructure.com/courses/20736/pages/load-test-page-110-migration-behavior-2" + ], + [ + "module_item", + "load-test-module-11|load-test-page-109", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "page", + "load-test-page-100", + "https://byu.instructure.com/courses/20736/pages/load-test-page-100-deep-sea-adaptation-2" + ], + [ + "page", + "load-test-page-080", + "https://byu.instructure.com/courses/20736/pages/load-test-page-080-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-12|load-test-page-119", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "page", + "load-test-page-090", + "https://byu.instructure.com/courses/20736/pages/load-test-page-090-food-web-transfer-2" + ], + [ + "module_item", + "load-test-module-10|load-test-page-099", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "module_item", + "load-test-module-09|load-test-page-089", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "page", + "load-test-page-070", + "https://byu.instructure.com/courses/20736/pages/load-test-page-070-benthic-biodiversity-2" + ], + [ + "module_item", + "load-test-module-08|load-test-page-079", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "module_item", + "load-test-module-07|load-test-page-069", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "page", + "load-test-page-060", + "https://byu.instructure.com/courses/20736/pages/load-test-page-060-nutrient-cycling-2" + ], + [ + "module_item", + "load-test-module-06|load-test-page-059", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "page", + "load-test-page-050", + "https://byu.instructure.com/courses/20736/pages/load-test-page-050-marine-conservation-policy-2" + ], + [ + "module_item", + "load-test-module-05|load-test-page-049", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "page", + "load-test-page-030", + "https://byu.instructure.com/courses/20736/pages/load-test-page-030-coastal-resilience-2" + ], + [ + "module_item", + "load-test-module-04|load-test-page-039", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "page", + "load-test-page-040", + "https://byu.instructure.com/courses/20736/pages/load-test-page-040-food-web-transfer-2" + ], + [ + "page", + "load-test-page-020", + "https://byu.instructure.com/courses/20736/pages/load-test-page-020-coastal-resilience-2" + ], + [ + "module_item", + "load-test-module-02|load-test-page-019", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "page", + "load-test-page-010", + "https://byu.instructure.com/courses/20736/pages/load-test-page-010-predator-prey-balance-2" + ], + [ + "module_item", + "load-test-module-03|load-test-page-029", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "module_item", + "load-test-module-01|load-test-page-009", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "module_item", + "load-test-module-19|load-test-page-190", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "module_item", + "load-test-module-20|load-test-page-200", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "module_item", + "load-test-module-15|load-test-page-150", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "module_item", + "load-test-module-18|load-test-page-180", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "module_item", + "load-test-module-16|load-test-page-160", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "module_item", + "load-test-module-14|load-test-page-140", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "module_item", + "load-test-module-17|load-test-page-170", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "module_item", + "load-test-module-13|load-test-page-130", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "module_item", + "load-test-module-12|load-test-page-120", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "module_item", + "load-test-module-11|load-test-page-110", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "module_item", + "load-test-module-09|load-test-page-090", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "module_item", + "load-test-module-10|load-test-page-100", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "module_item", + "load-test-module-07|load-test-page-070", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "module_item", + "load-test-module-08|load-test-page-080", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "module_item", + "load-test-module-06|load-test-page-060", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "module_item", + "load-test-module-04|load-test-page-040", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "module_item", + "load-test-module-05|load-test-page-050", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "module_item", + "load-test-module-03|load-test-page-030", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "module_item", + "load-test-module-01|load-test-page-010", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "module_item", + "load-test-module-02|load-test-page-020", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "module_item", + "load-test-module-20|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "module_item", + "load-test-module-18|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "module_item", + "load-test-module-19|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "module_item", + "load-test-module-17|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "module_item", + "load-test-module-16|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "module_item", + "load-test-module-14|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "module_item", + "load-test-module-15|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "assignment", + "load-test-assignment-171", + "https://byu.instructure.com/courses/20736/assignments/1335191" + ], + [ + "assignment", + "load-test-assignment-151", + "https://byu.instructure.com/courses/20736/assignments/1335193" + ], + [ + "assignment", + "load-test-assignment-181", + "https://byu.instructure.com/courses/20736/assignments/1335192" + ], + [ + "module_item", + "load-test-module-13|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "assignment", + "load-test-assignment-161", + "https://byu.instructure.com/courses/20736/assignments/1335196" + ], + [ + "assignment", + "load-test-assignment-191", + "https://byu.instructure.com/courses/20736/assignments/1335194" + ], + [ + "module_item", + "load-test-module-12|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "assignment", + "load-test-assignment-141", + "https://byu.instructure.com/courses/20736/assignments/1335195" + ], + [ + "assignment", + "load-test-assignment-121", + "https://byu.instructure.com/courses/20736/assignments/1335198" + ], + [ + "module_item", + "load-test-module-11|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "module_item", + "load-test-module-09|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "assignment", + "load-test-assignment-131", + "https://byu.instructure.com/courses/20736/assignments/1335197" + ], + [ + "module_item", + "load-test-module-10|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "assignment", + "load-test-assignment-111", + "https://byu.instructure.com/courses/20736/assignments/1335199" + ], + [ + "assignment", + "load-test-assignment-081", + "https://byu.instructure.com/courses/20736/assignments/1335200" + ], + [ + "module_item", + "load-test-module-08|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "module_item", + "load-test-module-07|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "module_item", + "load-test-module-06|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "assignment", + "load-test-assignment-101", + "https://byu.instructure.com/courses/20736/assignments/1335201" + ], + [ + "assignment", + "load-test-assignment-091", + "https://byu.instructure.com/courses/20736/assignments/1335202" + ], + [ + "module_item", + "load-test-module-05|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "module_item", + "load-test-module-04|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "assignment", + "load-test-assignment-061", + "https://byu.instructure.com/courses/20736/assignments/1335204" + ], + [ + "module_item", + "load-test-module-03|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "assignment", + "load-test-assignment-071", + "https://byu.instructure.com/courses/20736/assignments/1335203" + ], + [ + "assignment", + "load-test-assignment-051", + "https://byu.instructure.com/courses/20736/assignments/1335205" + ], + [ + "module_item", + "load-test-module-02|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "assignment", + "load-test-assignment-041", + "https://byu.instructure.com/courses/20736/assignments/1335206" + ], + [ + "module_item", + "load-test-module-01|Synthetic Assignments", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "assignment", + "load-test-assignment-031", + "https://byu.instructure.com/courses/20736/assignments/1335207" + ], + [ + "module_item", + "unit-3|Week 1 Wrap-Up", + "https://byu.instructure.com/courses/20736#module_382504" + ], + [ + "assignment", + "load-test-assignment-011", + "https://byu.instructure.com/courses/20736/assignments/1335208" + ], + [ + "assignment", + "load-test-assignment-021", + "https://byu.instructure.com/courses/20736/assignments/1335209" + ], + [ + "module_item", + "load-test-module-20|load-test-assignment-191", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "assignment", + "load-test-assignment-001", + "https://byu.instructure.com/courses/20736/assignments/1335210" + ], + [ + "module_item", + "load-test-module-19|load-test-assignment-181", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "module_item", + "load-test-module-18|load-test-assignment-171", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "module_item", + "load-test-module-17|load-test-assignment-161", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "assignment", + "lab-notebook-week1", + "https://byu.instructure.com/courses/20736/assignments/1335211" + ], + [ + "module_item", + "load-test-module-16|load-test-assignment-151", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "assignment", + "load-test-assignment-182", + "https://byu.instructure.com/courses/20736/assignments/1335213" + ], + [ + "assignment", + "load-test-assignment-192", + "https://byu.instructure.com/courses/20736/assignments/1335212" + ], + [ + "module_item", + "load-test-module-14|load-test-assignment-131", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "assignment", + "load-test-assignment-162", + "https://byu.instructure.com/courses/20736/assignments/1335215" + ], + [ + "assignment", + "load-test-assignment-172", + "https://byu.instructure.com/courses/20736/assignments/1335214" + ], + [ + "module_item", + "load-test-module-15|load-test-assignment-141", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "assignment", + "load-test-assignment-142", + "https://byu.instructure.com/courses/20736/assignments/1335216" + ], + [ + "assignment", + "load-test-assignment-152", + "https://byu.instructure.com/courses/20736/assignments/1335217" + ], + [ + "assignment", + "load-test-assignment-132", + "https://byu.instructure.com/courses/20736/assignments/1335218" + ], + [ + "module_item", + "load-test-module-13|load-test-assignment-121", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "assignment", + "load-test-assignment-112", + "https://byu.instructure.com/courses/20736/assignments/1335219" + ], + [ + "module_item", + "load-test-module-12|load-test-assignment-111", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "module_item", + "load-test-module-10|load-test-assignment-091", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "module_item", + "load-test-module-11|load-test-assignment-101", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "assignment", + "load-test-assignment-122", + "https://byu.instructure.com/courses/20736/assignments/1335220" + ], + [ + "module_item", + "load-test-module-09|load-test-assignment-081", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "assignment", + "load-test-assignment-102", + "https://byu.instructure.com/courses/20736/assignments/1335221" + ], + [ + "assignment", + "load-test-assignment-092", + "https://byu.instructure.com/courses/20736/assignments/1335223" + ], + [ + "module_item", + "load-test-module-08|load-test-assignment-071", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "module_item", + "load-test-module-07|load-test-assignment-061", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "assignment", + "load-test-assignment-082", + "https://byu.instructure.com/courses/20736/assignments/1335222" + ], + [ + "module_item", + "load-test-module-06|load-test-assignment-051", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "module_item", + "load-test-module-05|load-test-assignment-041", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "assignment", + "load-test-assignment-072", + "https://byu.instructure.com/courses/20736/assignments/1335224" + ], + [ + "assignment", + "load-test-assignment-062", + "https://byu.instructure.com/courses/20736/assignments/1335225" + ], + [ + "module_item", + "load-test-module-04|load-test-assignment-031", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "assignment", + "load-test-assignment-052", + "https://byu.instructure.com/courses/20736/assignments/1335226" + ], + [ + "assignment", + "load-test-assignment-042", + "https://byu.instructure.com/courses/20736/assignments/1335227" + ], + [ + "module_item", + "load-test-module-02|load-test-assignment-011", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "module_item", + "load-test-module-03|load-test-assignment-021", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "assignment", + "load-test-assignment-032", + "https://byu.instructure.com/courses/20736/assignments/1335228" + ], + [ + "module_item", + "load-test-module-01|load-test-assignment-001", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "module_item", + "unit-4|day-9-header", + "https://byu.instructure.com/courses/20736#module_382505" + ], + [ + "assignment", + "load-test-assignment-022", + "https://byu.instructure.com/courses/20736/assignments/1335229" + ], + [ + "module_item", + "load-test-module-20|load-test-assignment-192", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "module_item", + "unit-3|lab-notebook-week1", + "https://byu.instructure.com/courses/20736#module_382504" + ], + [ + "assignment", + "load-test-assignment-012", + "https://byu.instructure.com/courses/20736/assignments/1335230" + ], + [ + "module_item", + "load-test-module-19|load-test-assignment-182", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "module_item", + "load-test-module-18|load-test-assignment-172", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "assignment", + "load-test-assignment-193", + "https://byu.instructure.com/courses/20736/assignments/1335231" + ], + [ + "module_item", + "load-test-module-17|load-test-assignment-162", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "assignment", + "load-test-assignment-183", + "https://byu.instructure.com/courses/20736/assignments/1335232" + ], + [ + "assignment", + "load-test-assignment-173", + "https://byu.instructure.com/courses/20736/assignments/1335233" + ], + [ + "module_item", + "load-test-module-15|load-test-assignment-142", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "module_item", + "load-test-module-14|load-test-assignment-132", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "module_item", + "load-test-module-16|load-test-assignment-152", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "module_item", + "load-test-module-13|load-test-assignment-122", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "quiz", + "day-9-prep-quiz", + "https://byu.instructure.com/courses/20736/quizzes/610837" + ], + [ + "assignment", + "load-test-assignment-163", + "https://byu.instructure.com/courses/20736/assignments/1335236" + ], + [ + "assignment", + "load-test-assignment-153", + "https://byu.instructure.com/courses/20736/assignments/1335237" + ], + [ + "assignment", + "load-test-assignment-002", + "https://byu.instructure.com/courses/20736/assignments/1335234" + ], + [ + "assignment", + "load-test-assignment-143", + "https://byu.instructure.com/courses/20736/assignments/1335238" + ], + [ + "assignment", + "load-test-assignment-123", + "https://byu.instructure.com/courses/20736/assignments/1335240" + ], + [ + "assignment", + "load-test-assignment-133", + "https://byu.instructure.com/courses/20736/assignments/1335239" + ], + [ + "module_item", + "load-test-module-12|load-test-assignment-112", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "module_item", + "load-test-module-09|load-test-assignment-082", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "module_item", + "load-test-module-10|load-test-assignment-092", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "assignment", + "load-test-assignment-113", + "https://byu.instructure.com/courses/20736/assignments/1335241" + ], + [ + "assignment", + "load-test-assignment-103", + "https://byu.instructure.com/courses/20736/assignments/1335242" + ], + [ + "module_item", + "load-test-module-11|load-test-assignment-102", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "module_item", + "load-test-module-07|load-test-assignment-062", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "module_item", + "load-test-module-08|load-test-assignment-072", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "assignment", + "load-test-assignment-093", + "https://byu.instructure.com/courses/20736/assignments/1335243" + ], + [ + "module_item", + "load-test-module-06|load-test-assignment-052", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "assignment", + "load-test-assignment-083", + "https://byu.instructure.com/courses/20736/assignments/1335244" + ], + [ + "module_item", + "load-test-module-05|load-test-assignment-042", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "assignment", + "load-test-assignment-063", + "https://byu.instructure.com/courses/20736/assignments/1335245" + ], + [ + "assignment", + "load-test-assignment-053", + "https://byu.instructure.com/courses/20736/assignments/1335247" + ], + [ + "module_item", + "load-test-module-04|load-test-assignment-032", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "assignment", + "load-test-assignment-073", + "https://byu.instructure.com/courses/20736/assignments/1335246" + ], + [ + "assignment", + "load-test-assignment-043", + "https://byu.instructure.com/courses/20736/assignments/1335248" + ], + [ + "module_item", + "load-test-module-03|load-test-assignment-022", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "module_item", + "load-test-module-02|load-test-assignment-012", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "assignment", + "load-test-assignment-033", + "https://byu.instructure.com/courses/20736/assignments/1335249" + ], + [ + "page", + "day-9-marine-mammals", + "https://byu.instructure.com/courses/20736/pages/day-9-marine-mammals-12" + ], + [ + "assignment", + "load-test-assignment-013", + "https://byu.instructure.com/courses/20736/assignments/1335251" + ], + [ + "assignment", + "load-test-assignment-023", + "https://byu.instructure.com/courses/20736/assignments/1335250" + ], + [ + "module_item", + "unit-3|day-6-header", + "https://byu.instructure.com/courses/20736#module_382504" + ], + [ + "module_item", + "load-test-module-01|load-test-assignment-002", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "module_item", + "unit-4|day-9-prep-quiz", + "https://byu.instructure.com/courses/20736#module_382505" + ], + [ + "assignment", + "load-test-assignment-003", + "https://byu.instructure.com/courses/20736/assignments/1335252" + ], + [ + "module_item", + "unit-1|day-1-header", + "https://byu.instructure.com/courses/20736#module_382502" + ], + [ + "module_item", + "load-test-module-20|load-test-assignment-193", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "module_item", + "load-test-module-18|load-test-assignment-173", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "module_item", + "load-test-module-19|load-test-assignment-183", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "module_item", + "load-test-module-16|load-test-assignment-153", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "quiz", + "day-6-prep-quiz", + "https://byu.instructure.com/courses/20736/quizzes/610838" + ], + [ + "module_item", + "load-test-module-17|load-test-assignment-163", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "assignment", + "load-test-assignment-194", + "https://byu.instructure.com/courses/20736/assignments/1335255" + ], + [ + "assignment", + "load-test-assignment-184", + "https://byu.instructure.com/courses/20736/assignments/1335256" + ], + [ + "assignment", + "load-test-assignment-174", + "https://byu.instructure.com/courses/20736/assignments/1335257" + ], + [ + "module_item", + "load-test-module-15|load-test-assignment-143", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "assignment", + "load-test-assignment-164", + "https://byu.instructure.com/courses/20736/assignments/1335258" + ], + [ + "quiz", + "day-1-prep-quiz", + "https://byu.instructure.com/courses/20736/quizzes/610839" + ], + [ + "module_item", + "load-test-module-14|load-test-assignment-133", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "assignment", + "load-test-assignment-154", + "https://byu.instructure.com/courses/20736/assignments/1335259" + ], + [ + "module_item", + "load-test-module-13|load-test-assignment-123", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "module_item", + "load-test-module-12|load-test-assignment-113", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "assignment", + "load-test-assignment-134", + "https://byu.instructure.com/courses/20736/assignments/1335261" + ], + [ + "assignment", + "load-test-assignment-144", + "https://byu.instructure.com/courses/20736/assignments/1335260" + ], + [ + "module_item", + "load-test-module-11|load-test-assignment-103", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "assignment", + "load-test-assignment-124", + "https://byu.instructure.com/courses/20736/assignments/1335262" + ], + [ + "module_item", + "load-test-module-10|load-test-assignment-093", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "module_item", + "load-test-module-08|load-test-assignment-073", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "module_item", + "load-test-module-09|load-test-assignment-083", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "assignment", + "load-test-assignment-094", + "https://byu.instructure.com/courses/20736/assignments/1335264" + ], + [ + "assignment", + "load-test-assignment-084", + "https://byu.instructure.com/courses/20736/assignments/1335265" + ], + [ + "assignment", + "load-test-assignment-114", + "https://byu.instructure.com/courses/20736/assignments/1335263" + ], + [ + "module_item", + "load-test-module-07|load-test-assignment-063", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "assignment", + "load-test-assignment-074", + "https://byu.instructure.com/courses/20736/assignments/1335267" + ], + [ + "module_item", + "load-test-module-05|load-test-assignment-043", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "assignment", + "load-test-assignment-064", + "https://byu.instructure.com/courses/20736/assignments/1335268" + ], + [ + "assignment", + "load-test-assignment-104", + "https://byu.instructure.com/courses/20736/assignments/1335266" + ], + [ + "assignment", + "load-test-assignment-054", + "https://byu.instructure.com/courses/20736/assignments/1335269" + ], + [ + "module_item", + "load-test-module-06|load-test-assignment-053", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "module_item", + "load-test-module-03|load-test-assignment-023", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "assignment", + "load-test-assignment-044", + "https://byu.instructure.com/courses/20736/assignments/1335270" + ], + [ + "module_item", + "load-test-module-04|load-test-assignment-033", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "assignment", + "load-test-assignment-024", + "https://byu.instructure.com/courses/20736/assignments/1335271" + ], + [ + "module_item", + "load-test-module-02|load-test-assignment-013", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "assignment", + "load-test-assignment-014", + "https://byu.instructure.com/courses/20736/assignments/1335272" + ], + [ + "assignment", + "load-test-assignment-034", + "https://byu.instructure.com/courses/20736/assignments/1335273" + ], + [ + "module_item", + "load-test-module-01|load-test-assignment-003", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "page", + "day-6-coral-reefs", + "https://byu.instructure.com/courses/20736/pages/day-6-coral-reefs-12" + ], + [ + "page", + "day-1-welcome", + "https://byu.instructure.com/courses/20736/pages/day-1-welcome-to-the-sea-12" + ], + [ + "module_item", + "unit-3|day-6-prep-quiz", + "https://byu.instructure.com/courses/20736#module_382504" + ], + [ + "module_item", + "unit-4|day-9-marine-mammals", + "https://byu.instructure.com/courses/20736#module_382505" + ], + [ + "module_item", + "unit-1|day-1-prep-quiz", + "https://byu.instructure.com/courses/20736#module_382502" + ], + [ + "assignment", + "load-test-assignment-004", + "https://byu.instructure.com/courses/20736/assignments/1335274" + ], + [ + "module_item", + "load-test-module-20|load-test-assignment-194", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "module_item", + "load-test-module-18|load-test-assignment-174", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "module_item", + "load-test-module-19|load-test-assignment-184", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "module_item", + "load-test-module-17|load-test-assignment-164", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "assignment", + "load-test-assignment-195", + "https://byu.instructure.com/courses/20736/assignments/1335275" + ], + [ + "assignment", + "group-presentation", + "https://byu.instructure.com/courses/20736/assignments/1335276" + ], + [ + "assignment", + "load-test-assignment-175", + "https://byu.instructure.com/courses/20736/assignments/1335277" + ], + [ + "module_item", + "load-test-module-16|load-test-assignment-154", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "assignment", + "load-test-assignment-185", + "https://byu.instructure.com/courses/20736/assignments/1335278" + ], + [ + "module_item", + "load-test-module-15|load-test-assignment-144", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "module_item", + "load-test-module-14|load-test-assignment-134", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "module_item", + "load-test-module-13|load-test-assignment-124", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "assignment", + "load-test-assignment-165", + "https://byu.instructure.com/courses/20736/assignments/1335279" + ], + [ + "assignment", + "load-test-assignment-145", + "https://byu.instructure.com/courses/20736/assignments/1335280" + ], + [ + "module_item", + "load-test-module-12|load-test-assignment-114", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "assignment", + "load-test-assignment-155", + "https://byu.instructure.com/courses/20736/assignments/1335281" + ], + [ + "assignment", + "load-test-assignment-135", + "https://byu.instructure.com/courses/20736/assignments/1335282" + ], + [ + "module_item", + "load-test-module-11|load-test-assignment-104", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "assignment", + "load-test-assignment-125", + "https://byu.instructure.com/courses/20736/assignments/1335283" + ], + [ + "module_item", + "load-test-module-09|load-test-assignment-084", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "module_item", + "load-test-module-10|load-test-assignment-094", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "assignment", + "load-test-assignment-115", + "https://byu.instructure.com/courses/20736/assignments/1335284" + ], + [ + "assignment", + "load-test-assignment-105", + "https://byu.instructure.com/courses/20736/assignments/1335285" + ], + [ + "assignment", + "load-test-assignment-085", + "https://byu.instructure.com/courses/20736/assignments/1335286" + ], + [ + "module_item", + "load-test-module-07|load-test-assignment-064", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "module_item", + "load-test-module-08|load-test-assignment-074", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "module_item", + "load-test-module-06|load-test-assignment-054", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "assignment", + "load-test-assignment-095", + "https://byu.instructure.com/courses/20736/assignments/1335287" + ], + [ + "module_item", + "load-test-module-05|load-test-assignment-044", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "assignment", + "load-test-assignment-075", + "https://byu.instructure.com/courses/20736/assignments/1335288" + ], + [ + "assignment", + "load-test-assignment-065", + "https://byu.instructure.com/courses/20736/assignments/1335289" + ], + [ + "module_item", + "load-test-module-04|load-test-assignment-034", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "module_item", + "load-test-module-03|load-test-assignment-024", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "assignment", + "load-test-assignment-045", + "https://byu.instructure.com/courses/20736/assignments/1335290" + ], + [ + "module_item", + "load-test-module-02|load-test-assignment-014", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "module_item", + "load-test-module-01|load-test-assignment-004", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "assignment", + "load-test-assignment-035", + "https://byu.instructure.com/courses/20736/assignments/1335292" + ], + [ + "assignment", + "load-test-assignment-055", + "https://byu.instructure.com/courses/20736/assignments/1335291" + ], + [ + "assignment", + "load-test-assignment-025", + "https://byu.instructure.com/courses/20736/assignments/1335293" + ], + [ + "module_item", + "unit-4|group-presentation", + "https://byu.instructure.com/courses/20736#module_382505" + ], + [ + "module_item", + "unit-2|day-4-header", + "https://byu.instructure.com/courses/20736#module_382503" + ], + [ + "assignment", + "load-test-assignment-015", + "https://byu.instructure.com/courses/20736/assignments/1335295" + ], + [ + "module_item", + "unit-3|day-6-coral-reefs", + "https://byu.instructure.com/courses/20736#module_382504" + ], + [ + "assignment", + "load-test-assignment-005", + "https://byu.instructure.com/courses/20736/assignments/1335294" + ], + [ + "module_item", + "unit-1|day-1-welcome", + "https://byu.instructure.com/courses/20736#module_382502" + ], + [ + "module_item", + "load-test-module-20|load-test-assignment-195", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "module_item", + "load-test-module-18|load-test-assignment-175", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "module_item", + "load-test-module-19|load-test-assignment-185", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "quiz", + "day-4-prep-quiz", + "https://byu.instructure.com/courses/20736/quizzes/610840" + ], + [ + "assignment", + "load-test-assignment-196", + "https://byu.instructure.com/courses/20736/assignments/1335297" + ], + [ + "module_item", + "load-test-module-17|load-test-assignment-165", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "module_item", + "load-test-module-16|load-test-assignment-155", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "module_item", + "load-test-module-15|load-test-assignment-145", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "module_item", + "load-test-module-14|load-test-assignment-135", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "assignment", + "load-test-assignment-186", + "https://byu.instructure.com/courses/20736/assignments/1335298" + ], + [ + "assignment", + "load-test-assignment-156", + "https://byu.instructure.com/courses/20736/assignments/1335301" + ], + [ + "assignment", + "load-test-assignment-176", + "https://byu.instructure.com/courses/20736/assignments/1335299" + ], + [ + "module_item", + "load-test-module-13|load-test-assignment-125", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "assignment", + "load-test-assignment-166", + "https://byu.instructure.com/courses/20736/assignments/1335300" + ], + [ + "module_item", + "load-test-module-12|load-test-assignment-115", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "assignment", + "load-test-assignment-146", + "https://byu.instructure.com/courses/20736/assignments/1335302" + ], + [ + "assignment", + "load-test-assignment-126", + "https://byu.instructure.com/courses/20736/assignments/1335303" + ], + [ + "module_item", + "load-test-module-11|load-test-assignment-105", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "assignment", + "load-test-assignment-136", + "https://byu.instructure.com/courses/20736/assignments/1335304" + ], + [ + "assignment", + "load-test-assignment-116", + "https://byu.instructure.com/courses/20736/assignments/1335305" + ], + [ + "module_item", + "load-test-module-10|load-test-assignment-095", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "module_item", + "load-test-module-08|load-test-assignment-075", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "module_item", + "load-test-module-09|load-test-assignment-085", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "assignment", + "load-test-assignment-106", + "https://byu.instructure.com/courses/20736/assignments/1335306" + ], + [ + "module_item", + "load-test-module-07|load-test-assignment-065", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "assignment", + "load-test-assignment-096", + "https://byu.instructure.com/courses/20736/assignments/1335307" + ], + [ + "assignment", + "load-test-assignment-076", + "https://byu.instructure.com/courses/20736/assignments/1335308" + ], + [ + "module_item", + "load-test-module-06|load-test-assignment-055", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "module_item", + "load-test-module-05|load-test-assignment-045", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "assignment", + "load-test-assignment-066", + "https://byu.instructure.com/courses/20736/assignments/1335309" + ], + [ + "module_item", + "load-test-module-04|load-test-assignment-035", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "assignment", + "load-test-assignment-086", + "https://byu.instructure.com/courses/20736/assignments/1335310" + ], + [ + "assignment", + "load-test-assignment-056", + "https://byu.instructure.com/courses/20736/assignments/1335311" + ], + [ + "module_item", + "load-test-module-03|load-test-assignment-025", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "assignment", + "load-test-assignment-046", + "https://byu.instructure.com/courses/20736/assignments/1335312" + ], + [ + "assignment", + "load-test-assignment-026", + "https://byu.instructure.com/courses/20736/assignments/1335313" + ], + [ + "module_item", + "load-test-module-01|load-test-assignment-005", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "module_item", + "unit-4|day-10-header", + "https://byu.instructure.com/courses/20736#module_382505" + ], + [ + "module_item", + "load-test-module-02|load-test-assignment-015", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "assignment", + "load-test-assignment-016", + "https://byu.instructure.com/courses/20736/assignments/1335314" + ], + [ + "module_item", + "unit-3|day-7-header", + "https://byu.instructure.com/courses/20736#module_382504" + ], + [ + "page", + "day-4-phytoplankton", + "https://byu.instructure.com/courses/20736/pages/day-4-phytoplankton-and-primary-production-12" + ], + [ + "module_item", + "unit-2|day-4-prep-quiz", + "https://byu.instructure.com/courses/20736#module_382503" + ], + [ + "assignment", + "load-test-assignment-006", + "https://byu.instructure.com/courses/20736/assignments/1335316" + ], + [ + "module_item", + "unit-1|day-2-header", + "https://byu.instructure.com/courses/20736#module_382502" + ], + [ + "assignment", + "load-test-assignment-036", + "https://byu.instructure.com/courses/20736/assignments/1335315" + ], + [ + "module_item", + "load-test-module-20|load-test-assignment-196", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "module_item", + "load-test-module-19|load-test-assignment-186", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "assignment", + "load-test-assignment-197", + "https://byu.instructure.com/courses/20736/assignments/1335320" + ], + [ + "quiz", + "day-10-prep-quiz", + "https://byu.instructure.com/courses/20736/quizzes/610841" + ], + [ + "module_item", + "load-test-module-18|load-test-assignment-176", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "assignment", + "load-test-assignment-187", + "https://byu.instructure.com/courses/20736/assignments/1335321" + ], + [ + "module_item", + "load-test-module-17|load-test-assignment-166", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "quiz", + "day-7-prep-quiz", + "https://byu.instructure.com/courses/20736/quizzes/610842" + ], + [ + "module_item", + "load-test-module-16|load-test-assignment-156", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "assignment", + "load-test-assignment-177", + "https://byu.instructure.com/courses/20736/assignments/1335322" + ], + [ + "assignment", + "load-test-assignment-167", + "https://byu.instructure.com/courses/20736/assignments/1335323" + ], + [ + "assignment", + "load-test-assignment-157", + "https://byu.instructure.com/courses/20736/assignments/1335324" + ], + [ + "module_item", + "load-test-module-15|load-test-assignment-146", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "module_item", + "load-test-module-14|load-test-assignment-136", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "assignment", + "load-test-assignment-147", + "https://byu.instructure.com/courses/20736/assignments/1335325" + ], + [ + "quiz", + "day-2-prep-quiz", + "https://byu.instructure.com/courses/20736/quizzes/610843" + ], + [ + "module_item", + "load-test-module-13|load-test-assignment-126", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "module_item", + "load-test-module-12|load-test-assignment-116", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "module_item", + "load-test-module-11|load-test-assignment-106", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "module_item", + "load-test-module-09|load-test-assignment-086", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "module_item", + "load-test-module-10|load-test-assignment-096", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "assignment", + "load-test-assignment-127", + "https://byu.instructure.com/courses/20736/assignments/1335328" + ], + [ + "assignment", + "load-test-assignment-137", + "https://byu.instructure.com/courses/20736/assignments/1335326" + ], + [ + "assignment", + "load-test-assignment-117", + "https://byu.instructure.com/courses/20736/assignments/1335327" + ], + [ + "assignment", + "load-test-assignment-107", + "https://byu.instructure.com/courses/20736/assignments/1335329" + ], + [ + "module_item", + "load-test-module-08|load-test-assignment-076", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "module_item", + "load-test-module-07|load-test-assignment-066", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "assignment", + "load-test-assignment-097", + "https://byu.instructure.com/courses/20736/assignments/1335330" + ], + [ + "assignment", + "load-test-assignment-087", + "https://byu.instructure.com/courses/20736/assignments/1335331" + ], + [ + "assignment", + "load-test-assignment-077", + "https://byu.instructure.com/courses/20736/assignments/1335332" + ], + [ + "module_item", + "load-test-module-05|load-test-assignment-046", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "module_item", + "load-test-module-06|load-test-assignment-056", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "assignment", + "load-test-assignment-067", + "https://byu.instructure.com/courses/20736/assignments/1335333" + ], + [ + "assignment", + "load-test-assignment-047", + "https://byu.instructure.com/courses/20736/assignments/1335334" + ], + [ + "module_item", + "load-test-module-03|load-test-assignment-026", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "assignment", + "load-test-assignment-057", + "https://byu.instructure.com/courses/20736/assignments/1335335" + ], + [ + "module_item", + "load-test-module-04|load-test-assignment-036", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "module_item", + "load-test-module-02|load-test-assignment-016", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "assignment", + "load-test-assignment-027", + "https://byu.instructure.com/courses/20736/assignments/1335336" + ], + [ + "page", + "day-10-sharks-rays", + "https://byu.instructure.com/courses/20736/pages/day-10-sharks-rays-and-sea-turtles-12" + ], + [ + "assignment", + "load-test-assignment-037", + "https://byu.instructure.com/courses/20736/assignments/1335337" + ], + [ + "page", + "day-7-estuaries-mangroves", + "https://byu.instructure.com/courses/20736/pages/day-7-estuaries-and-mangroves-12" + ], + [ + "module_item", + "load-test-module-01|load-test-assignment-006", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "assignment", + "load-test-assignment-017", + "https://byu.instructure.com/courses/20736/assignments/1335338" + ], + [ + "page", + "day-2-ocean-geography", + "https://byu.instructure.com/courses/20736/pages/day-2-ocean-geography-12" + ], + [ + "module_item", + "unit-4|day-10-prep-quiz", + "https://byu.instructure.com/courses/20736#module_382505" + ], + [ + "assignment", + "load-test-assignment-007", + "https://byu.instructure.com/courses/20736/assignments/1335339" + ], + [ + "module_item", + "unit-2|day-4-phytoplankton", + "https://byu.instructure.com/courses/20736#module_382503" + ], + [ + "module_item", + "unit-1|day-2-prep-quiz", + "https://byu.instructure.com/courses/20736#module_382502" + ], + [ + "module_item", + "unit-3|day-7-prep-quiz", + "https://byu.instructure.com/courses/20736#module_382504" + ], + [ + "module_item", + "load-test-module-20|load-test-assignment-197", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "module_item", + "load-test-module-18|load-test-assignment-177", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "module_item", + "load-test-module-19|load-test-assignment-187", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "assignment", + "load-test-assignment-198", + "https://byu.instructure.com/courses/20736/assignments/1335340" + ], + [ + "assignment", + "load-test-assignment-178", + "https://byu.instructure.com/courses/20736/assignments/1335341" + ], + [ + "module_item", + "load-test-module-17|load-test-assignment-167", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "module_item", + "load-test-module-16|load-test-assignment-157", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "assignment", + "load-test-assignment-168", + "https://byu.instructure.com/courses/20736/assignments/1335342" + ], + [ + "module_item", + "load-test-module-15|load-test-assignment-147", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "module_item", + "load-test-module-14|load-test-assignment-137", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "module_item", + "load-test-module-13|load-test-assignment-127", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "assignment", + "load-test-assignment-158", + "https://byu.instructure.com/courses/20736/assignments/1335343" + ], + [ + "assignment", + "load-test-assignment-138", + "https://byu.instructure.com/courses/20736/assignments/1335344" + ], + [ + "assignment", + "load-test-assignment-148", + "https://byu.instructure.com/courses/20736/assignments/1335345" + ], + [ + "module_item", + "load-test-module-12|load-test-assignment-117", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "assignment", + "load-test-assignment-128", + "https://byu.instructure.com/courses/20736/assignments/1335346" + ], + [ + "module_item", + "load-test-module-11|load-test-assignment-107", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "assignment", + "load-test-assignment-118", + "https://byu.instructure.com/courses/20736/assignments/1335347" + ], + [ + "module_item", + "load-test-module-09|load-test-assignment-087", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "assignment", + "load-test-assignment-108", + "https://byu.instructure.com/courses/20736/assignments/1335348" + ], + [ + "module_item", + "load-test-module-10|load-test-assignment-097", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "assignment", + "load-test-assignment-098", + "https://byu.instructure.com/courses/20736/assignments/1335349" + ], + [ + "module_item", + "load-test-module-08|load-test-assignment-077", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "module_item", + "load-test-module-07|load-test-assignment-067", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "module_item", + "load-test-module-06|load-test-assignment-057", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "assignment", + "load-test-assignment-088", + "https://byu.instructure.com/courses/20736/assignments/1335350" + ], + [ + "assignment", + "load-test-assignment-078", + "https://byu.instructure.com/courses/20736/assignments/1335351" + ], + [ + "module_item", + "load-test-module-04|load-test-assignment-037", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "assignment", + "load-test-assignment-188", + "https://byu.instructure.com/courses/20736/assignments/1335352" + ], + [ + "assignment", + "load-test-assignment-068", + "https://byu.instructure.com/courses/20736/assignments/1335353" + ], + [ + "assignment", + "load-test-assignment-058", + "https://byu.instructure.com/courses/20736/assignments/1335354" + ], + [ + "module_item", + "load-test-module-05|load-test-assignment-047", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "module_item", + "load-test-module-03|load-test-assignment-027", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "assignment", + "load-test-assignment-048", + "https://byu.instructure.com/courses/20736/assignments/1335355" + ], + [ + "assignment", + "load-test-assignment-038", + "https://byu.instructure.com/courses/20736/assignments/1335356" + ], + [ + "module_item", + "load-test-module-01|load-test-assignment-007", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "module_item", + "load-test-module-02|load-test-assignment-017", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "assignment", + "load-test-assignment-018", + "https://byu.instructure.com/courses/20736/assignments/1335357" + ], + [ + "module_item", + "unit-4|day-10-sharks-rays", + "https://byu.instructure.com/courses/20736#module_382505" + ], + [ + "module_item", + "unit-2|day-5-header", + "https://byu.instructure.com/courses/20736#module_382503" + ], + [ + "assignment", + "load-test-assignment-028", + "https://byu.instructure.com/courses/20736/assignments/1335359" + ], + [ + "assignment", + "load-test-assignment-008", + "https://byu.instructure.com/courses/20736/assignments/1335358" + ], + [ + "module_item", + "load-test-module-20|load-test-assignment-198", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "module_item", + "unit-1|day-2-ocean-geography", + "https://byu.instructure.com/courses/20736#module_382502" + ], + [ + "module_item", + "unit-3|day-7-estuaries-mangroves", + "https://byu.instructure.com/courses/20736#module_382504" + ], + [ + "module_item", + "load-test-module-19|load-test-assignment-188", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "assignment", + "load-test-assignment-199", + "https://byu.instructure.com/courses/20736/assignments/1335361" + ], + [ + "quiz", + "day-5-prep-quiz", + "https://byu.instructure.com/courses/20736/quizzes/610844" + ], + [ + "module_item", + "load-test-module-18|load-test-assignment-178", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "module_item", + "load-test-module-17|load-test-assignment-168", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "assignment", + "load-test-assignment-189", + "https://byu.instructure.com/courses/20736/assignments/1335362" + ], + [ + "module_item", + "load-test-module-16|load-test-assignment-158", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "assignment", + "load-test-assignment-179", + "https://byu.instructure.com/courses/20736/assignments/1335363" + ], + [ + "module_item", + "load-test-module-15|load-test-assignment-148", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "assignment", + "load-test-assignment-169", + "https://byu.instructure.com/courses/20736/assignments/1335364" + ], + [ + "assignment", + "load-test-assignment-159", + "https://byu.instructure.com/courses/20736/assignments/1335365" + ], + [ + "module_item", + "load-test-module-13|load-test-assignment-128", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "module_item", + "load-test-module-14|load-test-assignment-138", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "assignment", + "load-test-assignment-149", + "https://byu.instructure.com/courses/20736/assignments/1335366" + ], + [ + "assignment", + "load-test-assignment-139", + "https://byu.instructure.com/courses/20736/assignments/1335367" + ], + [ + "module_item", + "load-test-module-11|load-test-assignment-108", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "assignment", + "load-test-assignment-129", + "https://byu.instructure.com/courses/20736/assignments/1335368" + ], + [ + "assignment", + "load-test-assignment-119", + "https://byu.instructure.com/courses/20736/assignments/1335369" + ], + [ + "module_item", + "load-test-module-10|load-test-assignment-098", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "module_item", + "load-test-module-08|load-test-assignment-078", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "module_item", + "load-test-module-09|load-test-assignment-088", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "assignment", + "load-test-assignment-109", + "https://byu.instructure.com/courses/20736/assignments/1335370" + ], + [ + "assignment", + "load-test-assignment-099", + "https://byu.instructure.com/courses/20736/assignments/1335371" + ], + [ + "module_item", + "load-test-module-07|load-test-assignment-068", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "module_item", + "load-test-module-12|load-test-assignment-118", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "assignment", + "load-test-assignment-089", + "https://byu.instructure.com/courses/20736/assignments/1335372" + ], + [ + "module_item", + "load-test-module-06|load-test-assignment-058", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "assignment", + "load-test-assignment-079", + "https://byu.instructure.com/courses/20736/assignments/1335373" + ], + [ + "module_item", + "load-test-module-04|load-test-assignment-038", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "module_item", + "load-test-module-05|load-test-assignment-048", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "assignment", + "load-test-assignment-069", + "https://byu.instructure.com/courses/20736/assignments/1335374" + ], + [ + "assignment", + "load-test-assignment-049", + "https://byu.instructure.com/courses/20736/assignments/1335375" + ], + [ + "module_item", + "load-test-module-03|load-test-assignment-028", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "assignment", + "load-test-assignment-059", + "https://byu.instructure.com/courses/20736/assignments/1335376" + ], + [ + "module_item", + "load-test-module-02|load-test-assignment-018", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "assignment", + "load-test-assignment-039", + "https://byu.instructure.com/courses/20736/assignments/1335377" + ], + [ + "module_item", + "load-test-module-01|load-test-assignment-008", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "module_item", + "unit-4|Final Assessments", + "https://byu.instructure.com/courses/20736#module_382505" + ], + [ + "page", + "day-5-zooplankton", + "https://byu.instructure.com/courses/20736/pages/day-5-zooplankton-and-food-webs-12" + ], + [ + "assignment", + "load-test-assignment-029", + "https://byu.instructure.com/courses/20736/assignments/1335378" + ], + [ + "module_item", + "unit-3|day-8-header", + "https://byu.instructure.com/courses/20736#module_382504" + ], + [ + "assignment", + "load-test-assignment-019", + "https://byu.instructure.com/courses/20736/assignments/1335379" + ], + [ + "assignment", + "load-test-assignment-009", + "https://byu.instructure.com/courses/20736/assignments/1335380" + ], + [ + "assignment", + "lab-notebook-week2", + "https://byu.instructure.com/courses/20736/assignments/1335382" + ], + [ + "module_item", + "unit-1|day-3-header", + "https://byu.instructure.com/courses/20736#module_382502" + ], + [ + "module_item", + "unit-2|day-5-prep-quiz", + "https://byu.instructure.com/courses/20736#module_382503" + ], + [ + "quiz", + "day-8-prep-quiz", + "https://byu.instructure.com/courses/20736/quizzes/610846" + ], + [ + "quiz", + "comprehensive-marine-biology-quiz", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "module_item", + "load-test-module-20|load-test-assignment-199", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "module_item", + "load-test-module-19|load-test-assignment-189", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "assignment", + "load-test-assignment-200", + "https://byu.instructure.com/courses/20736/assignments/1335385" + ], + [ + "quiz", + "day-3-prep-quiz", + "https://byu.instructure.com/courses/20736/quizzes/610847" + ], + [ + "assignment", + "load-test-assignment-190", + "https://byu.instructure.com/courses/20736/assignments/1335386" + ], + [ + "module_item", + "load-test-module-18|load-test-assignment-179", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "quiz", + "midweek-quiz", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "assignment", + "load-test-assignment-180", + "https://byu.instructure.com/courses/20736/assignments/1335387" + ], + [ + "module_item", + "load-test-module-16|load-test-assignment-159", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "assignment", + "load-test-assignment-170", + "https://byu.instructure.com/courses/20736/assignments/1335388" + ], + [ + "module_item", + "load-test-module-17|load-test-assignment-169", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "module_item", + "load-test-module-15|load-test-assignment-149", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "module_item", + "load-test-module-14|load-test-assignment-139", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "module_item", + "load-test-module-13|load-test-assignment-129", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "assignment", + "load-test-assignment-160", + "https://byu.instructure.com/courses/20736/assignments/1335389" + ], + [ + "assignment", + "load-test-assignment-150", + "https://byu.instructure.com/courses/20736/assignments/1335390" + ], + [ + "assignment", + "load-test-assignment-140", + "https://byu.instructure.com/courses/20736/assignments/1335391" + ], + [ + "module_item", + "load-test-module-11|load-test-assignment-109", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "module_item", + "load-test-module-12|load-test-assignment-119", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "assignment", + "load-test-assignment-130", + "https://byu.instructure.com/courses/20736/assignments/1335392" + ], + [ + "assignment", + "load-test-assignment-120", + "https://byu.instructure.com/courses/20736/assignments/1335393" + ], + [ + "module_item", + "load-test-module-10|load-test-assignment-099", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "module_item", + "load-test-module-09|load-test-assignment-089", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "module_item", + "load-test-module-08|load-test-assignment-079", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "assignment", + "load-test-assignment-110", + "https://byu.instructure.com/courses/20736/assignments/1335394" + ], + [ + "module_item", + "load-test-module-07|load-test-assignment-069", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "assignment", + "load-test-assignment-100", + "https://byu.instructure.com/courses/20736/assignments/1335395" + ], + [ + "assignment", + "load-test-assignment-080", + "https://byu.instructure.com/courses/20736/assignments/1335397" + ], + [ + "module_item", + "load-test-module-06|load-test-assignment-059", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "module_item", + "load-test-module-05|load-test-assignment-049", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "assignment", + "load-test-assignment-070", + "https://byu.instructure.com/courses/20736/assignments/1335398" + ], + [ + "assignment", + "load-test-assignment-090", + "https://byu.instructure.com/courses/20736/assignments/1335396" + ], + [ + "assignment", + "load-test-assignment-060", + "https://byu.instructure.com/courses/20736/assignments/1335399" + ], + [ + "module_item", + "load-test-module-04|load-test-assignment-039", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "module_item", + "load-test-module-03|load-test-assignment-029", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "assignment", + "load-test-assignment-050", + "https://byu.instructure.com/courses/20736/assignments/1335400" + ], + [ + "module_item", + "load-test-module-02|load-test-assignment-019", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "module_item", + "load-test-module-01|load-test-assignment-009", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "assignment", + "load-test-assignment-040", + "https://byu.instructure.com/courses/20736/assignments/1335401" + ], + [ + "assignment", + "load-test-assignment-030", + "https://byu.instructure.com/courses/20736/assignments/1335402" + ], + [ + "module_item", + "comprehensive-review|Course-Spanning Practice", + "https://byu.instructure.com/courses/20736#module_382506" + ], + [ + "page", + "day-8-deep-sea", + "https://byu.instructure.com/courses/20736/pages/day-8-the-deep-sea-12" + ], + [ + "assignment", + "load-test-assignment-020", + "https://byu.instructure.com/courses/20736/assignments/1335403" + ], + [ + "module_item", + "unit-4|lab-notebook-week2", + "https://byu.instructure.com/courses/20736#module_382505" + ], + [ + "assignment", + "load-test-assignment-010", + "https://byu.instructure.com/courses/20736/assignments/1335404" + ], + [ + "module_item", + "unit-3|day-8-prep-quiz", + "https://byu.instructure.com/courses/20736#module_382504" + ], + [ + "module_item", + "unit-2|day-5-zooplankton", + "https://byu.instructure.com/courses/20736#module_382503" + ], + [ + "page", + "day-3-seawater-chemistry", + "https://byu.instructure.com/courses/20736/pages/day-3-seawater-chemistry-12" + ], + [ + "module_item", + "unit-1|day-3-prep-quiz", + "https://byu.instructure.com/courses/20736#module_382502" + ], + [ + "assignment", + "final-reflection-essay", + "https://byu.instructure.com/courses/20736/assignments/1335405" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q196", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q189", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q197", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q192", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q193", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q199", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q195", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q188", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q190", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q191", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q194", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q198", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q187", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q186", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q184", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q181", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q183", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q180", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q179", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q185", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q176", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q182", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q177", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q178", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q175", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q174", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q170", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q172", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q171", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q168", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q173", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q167", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q169", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q165", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q166", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q164", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q163", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q162", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q161", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q158", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q156", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q160", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q159", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q157", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q155", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q154", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q153", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q151", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q147", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q146", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q145", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q152", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q150", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q144", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q149", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q148", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q142", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q141", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q143", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q136", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q140", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q139", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q138", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q132", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q137", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q134", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q135", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q133", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q131", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q130", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q126", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q125", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q122", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q128", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q129", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q127", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q123", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q124", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q120", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q121", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q119", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q116", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q118", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q117", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q115", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q110", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q108", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q113", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q112", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q114", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q107", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q109", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q111", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q105", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q103", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q106", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q104", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q101", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q102", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q99", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q100", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q94", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q98", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q93", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q95", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q92", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q96", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q97", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q91", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q90", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q87", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q89", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q88", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q83", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q80", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q86", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q79", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q78", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q84", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q85", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q82", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q81", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q75", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q77", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q74", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q76", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q67", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q73", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q69", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q72", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q71", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q70", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q66", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q68", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q65", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q62", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q64", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q60", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q63", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q57", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q56", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q55", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q58", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q61", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q59", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q53", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q54", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q50", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q48", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q49", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q47", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q52", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q51", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q43", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q44", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q42", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q46", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q45", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q40", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q38", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q36", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q41", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q33", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q39", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q31", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q37", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q35", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q30", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q34", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q32", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q29", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q27", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q26", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q28", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q24", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q25", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q19", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q21", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q20", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q18", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q23", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q22", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q17", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q14", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q13", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q16", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q15", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q12", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q10", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q11", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q7", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q6", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q5", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q9", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q8", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q1", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q2", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q3", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q4", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "midweek-quiz|q11", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question", + "comprehensive-marine-biology-quiz|q0", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "midweek-quiz|q12", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question", + "midweek-quiz|q13", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question", + "midweek-quiz|q14", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question", + "midweek-quiz|q10", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question", + "midweek-quiz|q9", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question", + "midweek-quiz|q7", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question", + "midweek-quiz|q8", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question", + "midweek-quiz|q6", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question", + "midweek-quiz|q1", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question", + "midweek-quiz|q3", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question", + "midweek-quiz|q2", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question", + "day-10-prep-quiz|q6", + "https://byu.instructure.com/courses/20736/quizzes/610841" + ], + [ + "quiz_question", + "midweek-quiz|q4", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question", + "midweek-quiz|q5", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question", + "midweek-quiz|q0", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question", + "day-10-prep-quiz|q5", + "https://byu.instructure.com/courses/20736/quizzes/610841" + ], + [ + "quiz_question", + "day-10-prep-quiz|q3", + "https://byu.instructure.com/courses/20736/quizzes/610841" + ], + [ + "quiz_question", + "day-10-prep-quiz|q4", + "https://byu.instructure.com/courses/20736/quizzes/610841" + ], + [ + "quiz_question", + "day-10-prep-quiz|q2", + "https://byu.instructure.com/courses/20736/quizzes/610841" + ], + [ + "quiz_question", + "day-9-prep-quiz|q6", + "https://byu.instructure.com/courses/20736/quizzes/610837" + ], + [ + "quiz_question", + "day-9-prep-quiz|q3", + "https://byu.instructure.com/courses/20736/quizzes/610837" + ], + [ + "quiz_question", + "day-9-prep-quiz|q2", + "https://byu.instructure.com/courses/20736/quizzes/610837" + ], + [ + "quiz_question", + "day-10-prep-quiz|q1", + "https://byu.instructure.com/courses/20736/quizzes/610841" + ], + [ + "quiz_question", + "day-10-prep-quiz|q0", + "https://byu.instructure.com/courses/20736/quizzes/610841" + ], + [ + "quiz_question", + "day-9-prep-quiz|q0", + "https://byu.instructure.com/courses/20736/quizzes/610837" + ], + [ + "quiz_question", + "day-9-prep-quiz|q5", + "https://byu.instructure.com/courses/20736/quizzes/610837" + ], + [ + "quiz_question", + "day-8-prep-quiz|q6", + "https://byu.instructure.com/courses/20736/quizzes/610846" + ], + [ + "quiz_question", + "day-9-prep-quiz|q4", + "https://byu.instructure.com/courses/20736/quizzes/610837" + ], + [ + "quiz_question", + "day-8-prep-quiz|q5", + "https://byu.instructure.com/courses/20736/quizzes/610846" + ], + [ + "quiz_question", + "day-9-prep-quiz|q1", + "https://byu.instructure.com/courses/20736/quizzes/610837" + ], + [ + "quiz_question", + "day-8-prep-quiz|q4", + "https://byu.instructure.com/courses/20736/quizzes/610846" + ], + [ + "quiz_question", + "day-8-prep-quiz|q1", + "https://byu.instructure.com/courses/20736/quizzes/610846" + ], + [ + "quiz_question", + "day-8-prep-quiz|q3", + "https://byu.instructure.com/courses/20736/quizzes/610846" + ], + [ + "quiz_question", + "day-8-prep-quiz|q2", + "https://byu.instructure.com/courses/20736/quizzes/610846" + ], + [ + "quiz_question", + "day-7-prep-quiz|q2", + "https://byu.instructure.com/courses/20736/quizzes/610842" + ], + [ + "quiz_question", + "day-8-prep-quiz|q0", + "https://byu.instructure.com/courses/20736/quizzes/610846" + ], + [ + "quiz_question", + "day-7-prep-quiz|q0", + "https://byu.instructure.com/courses/20736/quizzes/610842" + ], + [ + "quiz_question", + "day-7-prep-quiz|q6", + "https://byu.instructure.com/courses/20736/quizzes/610842" + ], + [ + "quiz_question", + "day-7-prep-quiz|q3", + "https://byu.instructure.com/courses/20736/quizzes/610842" + ], + [ + "quiz_question", + "day-7-prep-quiz|q5", + "https://byu.instructure.com/courses/20736/quizzes/610842" + ], + [ + "quiz_question", + "day-7-prep-quiz|q4", + "https://byu.instructure.com/courses/20736/quizzes/610842" + ], + [ + "quiz_question", + "day-6-prep-quiz|q6", + "https://byu.instructure.com/courses/20736/quizzes/610838" + ], + [ + "quiz_question", + "day-7-prep-quiz|q1", + "https://byu.instructure.com/courses/20736/quizzes/610842" + ], + [ + "quiz_question", + "day-6-prep-quiz|q5", + "https://byu.instructure.com/courses/20736/quizzes/610838" + ], + [ + "quiz_question", + "day-6-prep-quiz|q2", + "https://byu.instructure.com/courses/20736/quizzes/610838" + ], + [ + "quiz_question", + "day-6-prep-quiz|q1", + "https://byu.instructure.com/courses/20736/quizzes/610838" + ], + [ + "quiz_question", + "day-6-prep-quiz|q3", + "https://byu.instructure.com/courses/20736/quizzes/610838" + ], + [ + "quiz_question", + "day-5-prep-quiz|q5", + "https://byu.instructure.com/courses/20736/quizzes/610844" + ], + [ + "quiz_question", + "day-5-prep-quiz|q3", + "https://byu.instructure.com/courses/20736/quizzes/610844" + ], + [ + "quiz_question", + "day-5-prep-quiz|q4", + "https://byu.instructure.com/courses/20736/quizzes/610844" + ], + [ + "quiz_question", + "day-6-prep-quiz|q4", + "https://byu.instructure.com/courses/20736/quizzes/610838" + ], + [ + "quiz_question", + "day-5-prep-quiz|q6", + "https://byu.instructure.com/courses/20736/quizzes/610844" + ], + [ + "quiz_question", + "day-5-prep-quiz|q1", + "https://byu.instructure.com/courses/20736/quizzes/610844" + ], + [ + "quiz_question", + "day-6-prep-quiz|q0", + "https://byu.instructure.com/courses/20736/quizzes/610838" + ], + [ + "quiz_question", + "day-5-prep-quiz|q2", + "https://byu.instructure.com/courses/20736/quizzes/610844" + ], + [ + "quiz_question", + "day-4-prep-quiz|q4", + "https://byu.instructure.com/courses/20736/quizzes/610840" + ], + [ + "quiz_question", + "day-5-prep-quiz|q0", + "https://byu.instructure.com/courses/20736/quizzes/610844" + ], + [ + "quiz_question", + "day-4-prep-quiz|q6", + "https://byu.instructure.com/courses/20736/quizzes/610840" + ], + [ + "quiz_question", + "day-3-prep-quiz|q6", + "https://byu.instructure.com/courses/20736/quizzes/610847" + ], + [ + "quiz_question", + "day-4-prep-quiz|q3", + "https://byu.instructure.com/courses/20736/quizzes/610840" + ], + [ + "quiz_question", + "day-4-prep-quiz|q1", + "https://byu.instructure.com/courses/20736/quizzes/610840" + ], + [ + "quiz_question", + "day-4-prep-quiz|q5", + "https://byu.instructure.com/courses/20736/quizzes/610840" + ], + [ + "quiz_question", + "day-4-prep-quiz|q0", + "https://byu.instructure.com/courses/20736/quizzes/610840" + ], + [ + "quiz_question", + "day-3-prep-quiz|q4", + "https://byu.instructure.com/courses/20736/quizzes/610847" + ], + [ + "quiz_question", + "day-3-prep-quiz|q5", + "https://byu.instructure.com/courses/20736/quizzes/610847" + ], + [ + "quiz_question", + "day-4-prep-quiz|q2", + "https://byu.instructure.com/courses/20736/quizzes/610840" + ], + [ + "quiz_question", + "day-3-prep-quiz|q2", + "https://byu.instructure.com/courses/20736/quizzes/610847" + ], + [ + "quiz_question", + "day-3-prep-quiz|q1", + "https://byu.instructure.com/courses/20736/quizzes/610847" + ], + [ + "quiz_question", + "day-3-prep-quiz|q3", + "https://byu.instructure.com/courses/20736/quizzes/610847" + ], + [ + "quiz_question", + "day-2-prep-quiz|q4", + "https://byu.instructure.com/courses/20736/quizzes/610843" + ], + [ + "quiz_question", + "day-3-prep-quiz|q0", + "https://byu.instructure.com/courses/20736/quizzes/610847" + ], + [ + "quiz_question", + "day-1-prep-quiz|q6", + "https://byu.instructure.com/courses/20736/quizzes/610839" + ], + [ + "quiz_question", + "day-2-prep-quiz|q6", + "https://byu.instructure.com/courses/20736/quizzes/610843" + ], + [ + "quiz_question", + "day-2-prep-quiz|q5", + "https://byu.instructure.com/courses/20736/quizzes/610843" + ], + [ + "quiz_question", + "day-2-prep-quiz|q2", + "https://byu.instructure.com/courses/20736/quizzes/610843" + ], + [ + "module_item", + "load-test-module-20|load-test-assignment-200", + "https://byu.instructure.com/courses/20736#module_382526" + ], + [ + "quiz_question", + "day-2-prep-quiz|q3", + "https://byu.instructure.com/courses/20736/quizzes/610843" + ], + [ + "module_item", + "load-test-module-19|load-test-assignment-190", + "https://byu.instructure.com/courses/20736#module_382525" + ], + [ + "quiz_question", + "day-2-prep-quiz|q0", + "https://byu.instructure.com/courses/20736/quizzes/610843" + ], + [ + "module_item", + "load-test-module-18|load-test-assignment-180", + "https://byu.instructure.com/courses/20736#module_382524" + ], + [ + "module_item", + "load-test-module-17|load-test-assignment-170", + "https://byu.instructure.com/courses/20736#module_382523" + ], + [ + "module_item", + "load-test-module-16|load-test-assignment-160", + "https://byu.instructure.com/courses/20736#module_382522" + ], + [ + "module_item", + "load-test-module-15|load-test-assignment-150", + "https://byu.instructure.com/courses/20736#module_382521" + ], + [ + "quiz_question", + "day-2-prep-quiz|q1", + "https://byu.instructure.com/courses/20736/quizzes/610843" + ], + [ + "module_item", + "load-test-module-14|load-test-assignment-140", + "https://byu.instructure.com/courses/20736#module_382520" + ], + [ + "module_item", + "load-test-module-13|load-test-assignment-130", + "https://byu.instructure.com/courses/20736#module_382519" + ], + [ + "module_item", + "load-test-module-12|load-test-assignment-120", + "https://byu.instructure.com/courses/20736#module_382518" + ], + [ + "module_item", + "load-test-module-11|load-test-assignment-110", + "https://byu.instructure.com/courses/20736#module_382517" + ], + [ + "module_item", + "load-test-module-09|load-test-assignment-090", + "https://byu.instructure.com/courses/20736#module_382515" + ], + [ + "module_item", + "load-test-module-08|load-test-assignment-080", + "https://byu.instructure.com/courses/20736#module_382514" + ], + [ + "quiz_question", + "day-1-prep-quiz|q4", + "https://byu.instructure.com/courses/20736/quizzes/610839" + ], + [ + "module_item", + "load-test-module-10|load-test-assignment-100", + "https://byu.instructure.com/courses/20736#module_382516" + ], + [ + "module_item", + "load-test-module-07|load-test-assignment-070", + "https://byu.instructure.com/courses/20736#module_382513" + ], + [ + "module_item", + "load-test-module-06|load-test-assignment-060", + "https://byu.instructure.com/courses/20736#module_382512" + ], + [ + "module_item", + "load-test-module-05|load-test-assignment-050", + "https://byu.instructure.com/courses/20736#module_382511" + ], + [ + "quiz_question", + "day-1-prep-quiz|q3", + "https://byu.instructure.com/courses/20736/quizzes/610839" + ], + [ + "module_item", + "load-test-module-02|load-test-assignment-020", + "https://byu.instructure.com/courses/20736#module_382508" + ], + [ + "module_item", + "load-test-module-04|load-test-assignment-040", + "https://byu.instructure.com/courses/20736#module_382510" + ], + [ + "module_item", + "load-test-module-03|load-test-assignment-030", + "https://byu.instructure.com/courses/20736#module_382509" + ], + [ + "module_item", + "load-test-module-01|load-test-assignment-010", + "https://byu.instructure.com/courses/20736#module_382507" + ], + [ + "module_item", + "comprehensive-review|comprehensive-marine-biology-quiz", + "https://byu.instructure.com/courses/20736#module_382506" + ], + [ + "module_item", + "unit-4|final-reflection-essay", + "https://byu.instructure.com/courses/20736#module_382505" + ], + [ + "module_item", + "unit-3|day-8-deep-sea", + "https://byu.instructure.com/courses/20736#module_382504" + ], + [ + "module_item", + "unit-2|midweek-quiz", + "https://byu.instructure.com/courses/20736#module_382503" + ], + [ + "module_item", + "unit-1|day-3-seawater-chemistry", + "https://byu.instructure.com/courses/20736#module_382502" + ], + [ + "quiz_question_order", + "midweek-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610848" + ], + [ + "quiz_question_order", + "comprehensive-marine-biology-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610845" + ], + [ + "quiz_question", + "day-1-prep-quiz|q5", + "https://byu.instructure.com/courses/20736/quizzes/610839" + ], + [ + "quiz_question_order", + "day-10-prep-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610841" + ], + [ + "quiz_question_order", + "day-8-prep-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610846" + ], + [ + "quiz_question_order", + "day-9-prep-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610837" + ], + [ + "quiz_question_order", + "day-6-prep-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610838" + ], + [ + "quiz_question_order", + "day-7-prep-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610842" + ], + [ + "quiz_question_order", + "day-5-prep-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610844" + ], + [ + "quiz_question_order", + "day-4-prep-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610840" + ], + [ + "quiz_question_order", + "day-3-prep-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610847" + ], + [ + "quiz_question_order", + "day-2-prep-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610843" + ], + [ + "quiz_question", + "day-1-prep-quiz|q2", + "https://byu.instructure.com/courses/20736/quizzes/610839" + ], + [ + "quiz_question", + "day-1-prep-quiz|q1", + "https://byu.instructure.com/courses/20736/quizzes/610839" + ], + [ + "quiz_question", + "day-1-prep-quiz|q0", + "https://byu.instructure.com/courses/20736/quizzes/610839" + ], + [ + "syllabus", + "syllabus", + "https://byu.instructure.com/courses/20736/assignments/syllabus" + ], + [ + "announcement", + "welcome-announcement", + "https://byu.instructure.com/courses/20736/discussion_topics/687933" + ], + [ + "quiz_question_order", + "day-1-prep-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610839" + ] + ], + "content_to_review": [], + "error": "" +} \ No newline at end of file diff --git a/tests/test-urgent-mdxcanvas-payload.json b/tests/test-urgent-mdxcanvas-payload.json new file mode 100644 index 0000000..4d5be53 --- /dev/null +++ b/tests/test-urgent-mdxcanvas-payload.json @@ -0,0 +1,17 @@ +{ + "deployed_content": [ + [ + "quiz_question_order", + "day-2-prep-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610843" + ] + ], + "content_to_review": [ + [ + "quiz_question_order", + "day-4-prep-quiz|order", + "https://byu.instructure.com/courses/20736/quizzes/610844" + ] + ], + "error": "" +} \ No newline at end of file diff --git a/tests/test_course_text_formatters.py b/tests/test_course_text_formatters.py new file mode 100644 index 0000000..c370e09 --- /dev/null +++ b/tests/test_course_text_formatters.py @@ -0,0 +1,196 @@ +from notifications.formatting.canvas_format import format_notification as format_canvas_notification +from notifications.formatting.docker_format import format_notification as format_docker_notification +from notifications.discord_limits import validate_notification + + +class TestCanvasNotificationSuccess: + def test_success_has_overview_table(self): + notification = format_canvas_notification( + data={ + "deployed_content": [ + ("page", "Week 12 Overview", "https://courses.example/week-12"), + ("page", "Lab 8 Instructions", "https://courses.example/lab-8"), + ("assignment", "Project Milestone", "https://courses.example/project"), + ], + "content_to_review": [], + "error": "", + }, + course_id="235", + course_name="CS 235 Spring 2026", + course_url="https://courses.example/cs235", + author="robbykapua", + author_icon="https://github.com/robbykapua.png", + branch="main", + action_url="https://github.com/actions/runs/123", + ) + message = notification.messages[0] + embed = message.embeds[0] + + assert message.content is None + assert "Deploy complete" in embed.title + assert "`main`" in embed.description + assert embed.author is not None + assert embed.footer is not None + + # First field is overview table + overview = embed.fields[0] + assert "Overview" in overview.name + assert "page" in overview.value + assert "assignment" in overview.value + + # Remaining resources field follows + remaining = embed.fields[1] + assert "Remaining Resources" in remaining.name + assert "Week 12 Overview" in remaining.value + + assert validate_notification(notification) == [] + + +class TestCanvasNotificationReview: + def test_review_has_content_ping_and_review_section(self): + notification = format_canvas_notification( + data={ + "deployed_content": [ + ("page", "Week 12 Overview", "https://courses.example/week-12"), + ("assignment", "Needs Review", "https://courses.example/review-me"), + ], + "content_to_review": [ + ("assignment", "Needs Review", "https://courses.example/review-me"), + ("assignment", "Professor Approval", "https://courses.example/professor"), + ], + "error": "", + }, + course_id="235", + course_name="CS 235 Spring 2026", + course_url="https://courses.example/cs235", + author="robbykapua", + author_icon="https://github.com/robbykapua.png", + branch="main", + action_url="https://github.com/actions/runs/123", + cicd_role_id="123456", + ) + message = notification.messages[0] + embed = message.embeds[0] + + assert "<@&123456>" in message.content + assert "review" in embed.title.lower() + + # Has overview, needs review, and remaining fields + field_names = [f.name for f in embed.fields] + assert any("Overview" in n for n in field_names) + assert any("Needs review" in n for n in field_names) + assert any("Remaining Resources" in n for n in field_names) + + # Review items present + review_field = next(f for f in embed.fields if "Needs review" in f.name) + assert "Needs Review" in review_field.value + assert "Professor Approval" in review_field.value + + # Deduplication: review item not in remaining + remaining_field = next(f for f in embed.fields if "Remaining Resources" in f.name) + assert "https://courses.example/review-me" not in remaining_field.value + assert "Week 12 Overview" in remaining_field.value + + assert validate_notification(notification) == [] + + +class TestCanvasNotificationError: + def test_error_has_content_ping_and_error_in_description(self): + notification = format_canvas_notification( + data={ + "deployed_content": [], + "content_to_review": [], + "error": "\n".join([ + "prefix noise", + "Traceback (most recent call last):", + ' File "runner.py", line 1, in ', + " raise RuntimeError('boom')", + "RuntimeError: boom", + ]), + }, + course_id="235", + course_name="CS 235 Spring 2026", + course_url="https://courses.example/cs235", + author="robbykapua", + author_icon="", + branch="main", + action_url="https://github.com/actions/runs/123", + cicd_role_id="123456", + ) + message = notification.messages[0] + embed = message.embeds[0] + + assert "failed" in embed.title.lower() + assert "RuntimeError: boom" in embed.description + assert "<@&123456>" in message.content + assert "ERROR" in message.content + assert embed.fields == [] + + assert validate_notification(notification) == [] + + +class TestDockerNotificationFailures: + def test_docker_with_failures(self): + notification = format_docker_notification( + data={ + "updated_images": ["lab-1", "lab-2"], + "failed_images": ["project-base"], + "error": "", + }, + course_id="235", + course_name="CS 235 Spring 2026", + course_url="https://courses.example/cs235", + author="robbykapua", + author_icon="", + branch="main", + action_url="https://github.com/actions/runs/123", + ) + message = notification.messages[0] + embed = message.embeds[0] + + assert message.content is None + assert "failures" in embed.title.lower() + assert len(embed.fields) == 2 + + failed_field = embed.fields[0] + assert "Failed" in failed_field.name + assert "`project-base`" in failed_field.value + + built_field = embed.fields[1] + assert "Built" in built_field.name + assert "`lab-1`" in built_field.value + assert "`lab-2`" in built_field.value + + assert validate_notification(notification) == [] + + +class TestDockerNotificationError: + def test_docker_with_error(self): + notification = format_docker_notification( + data={ + "updated_images": [], + "failed_images": ["project-base"], + "error": "\n".join([ + "build logs", + "Traceback (most recent call last):", + ' File "docker.py", line 10, in ', + " raise ValueError('bad image')", + "ValueError: bad image", + ]), + }, + course_id="235", + course_name="CS 235 Spring 2026", + course_url="https://courses.example/cs235", + author="robbykapua", + author_icon="", + branch="main", + action_url="https://github.com/actions/runs/123", + ) + message = notification.messages[0] + embed = message.embeds[0] + + assert "failed" in embed.title.lower() + assert "ValueError: bad image" in embed.description + assert message.content is None + + assert validate_notification(notification) == [] diff --git a/tests/test_discord_limits.py b/tests/test_discord_limits.py new file mode 100644 index 0000000..62f9040 --- /dev/null +++ b/tests/test_discord_limits.py @@ -0,0 +1,446 @@ +from notifications.discord_limits import ( + TITLE_LIMIT, + DESCRIPTION_LIMIT, + FIELD_NAME_LIMIT, + FIELD_VALUE_LIMIT, + FIELDS_PER_EMBED, + FOOTER_LIMIT, + AUTHOR_NAME_LIMIT, + EMBED_CHAR_LIMIT, + MAX_EMBEDS_PER_MESSAGE, + CONTENT_LIMIT, + calc_embed_size, + split_content, + EmbedBuilder, + MessageBuilder, + validate_notification, +) +from notifications.resources import Author, Embed, Field, Footer, Notification, WebhookMessage + + +class TestConstants: + def test_embed_char_limit(self): + assert EMBED_CHAR_LIMIT == 6000 + + def test_field_value_limit(self): + assert FIELD_VALUE_LIMIT == 1024 + + def test_fields_per_embed(self): + assert FIELDS_PER_EMBED == 25 + + def test_content_limit(self): + assert CONTENT_LIMIT == 2000 + + def test_max_embeds_per_message(self): + assert MAX_EMBEDS_PER_MESSAGE == 10 + + +class TestCalcEmbedSize: + def test_basic_size(self): + embed = Embed( + title="Hello", + description="World", + color=0xFF0000, + fields=[Field(name="key", value="val")], + timestamp="2025-01-01T00:00:00Z", + ) + assert calc_embed_size(embed) == len("Hello") + len("World") + len("key") + len("val") + + def test_includes_footer_and_author(self): + embed = Embed( + title="T", + description="D", + color=0, + fields=[], + timestamp="", + author=Author(name="AuthorName"), + footer=Footer(text="FooterText"), + ) + assert calc_embed_size(embed) == len("T") + len("D") + len("AuthorName") + len("FooterText") + + def test_empty_embed(self): + embed = Embed( + title="", + description="", + color=0, + fields=[], + timestamp="", + ) + assert calc_embed_size(embed) == 0 + + +class TestSplitContent: + def test_short_content_unchanged(self): + result = split_content("hello", 2000) + assert result == ["hello"] + + def test_long_content_splits_on_paragraphs(self): + paragraphs = ["paragraph " + str(i) for i in range(200)] + content = "\n\n".join(paragraphs) + result = split_content(content, 100) + assert len(result) > 1 + for chunk in result: + assert len(chunk) <= 100 + + +class TestEmbedBuilder: + def test_build_produces_embed(self): + b = EmbedBuilder( + title="Title", + description="Desc", + color=0xFF0000, + timestamp="2025-01-01T00:00:00Z", + author=Author(name="Bot"), + footer=Footer(text="Footer"), + ) + embed = b.build() + assert embed.title == "Title" + assert embed.description == "Desc" + assert embed.color == 0xFF0000 + assert embed.author.name == "Bot" + assert embed.footer.text == "Footer" + assert embed.fields == [] + + def test_add_field_success(self): + b = EmbedBuilder( + title="T", description="D", color=0, timestamp="", + ) + result = b.add_field("Name", "Value", inline=False) + assert result is True + embed = b.build() + assert len(embed.fields) == 1 + assert embed.fields[0].name == "Name" + assert embed.fields[0].value == "Value" + + def test_can_add_field_checks_char_limit(self): + b = EmbedBuilder( + title="x" * 5000, description="", color=0, timestamp="", + ) + assert b.can_add_field("name", "x" * 1500) is False + + def test_can_add_field_checks_field_count(self): + b = EmbedBuilder( + title="T", description="D", color=0, timestamp="", + ) + for i in range(25): + b.add_field(f"f{i}", "v") + assert b.can_add_field("f25", "v") is False + + def test_add_field_returns_false_when_full(self): + b = EmbedBuilder( + title="x" * 5900, description="", color=0, timestamp="", + ) + result = b.add_field("name", "x" * 500) + assert result is False + assert len(b.build().fields) == 0 + + def test_remaining_chars_decreases(self): + b = EmbedBuilder( + title="Hello", description="World", color=0, timestamp="", + ) + initial = b.remaining_chars() + b.add_field("Key", "Value") + assert b.remaining_chars() == initial - len("Key") - len("Value") + + +class TestMessageBuilder: + def test_build_single_embed_message(self): + mb = MessageBuilder(color=0xFF0000, timestamp="2025-01-01T00:00:00Z") + eb = mb.new_embed(title="Title", description="Desc") + eb.add_field("Key", "Value") + messages = mb.build() + assert len(messages) == 1 + assert len(messages[0].embeds) == 1 + assert messages[0].embeds[0].title == "Title" + + def test_new_embed_creates_continuation(self): + mb = MessageBuilder(color=0xFF0000, timestamp="2025-01-01T00:00:00Z") + mb.new_embed(title="First", description="Desc") + mb.new_embed(title="Second", description="") + messages = mb.build() + # Both small embeds should fit in one message + total_embeds = sum(len(m.embeds) for m in messages) + assert total_embeds == 2 + + def test_set_content(self): + mb = MessageBuilder(color=0, timestamp="") + mb.set_content("hello world") + mb.new_embed(title="T", description="D") + messages = mb.build() + assert messages[0].content == "hello world" + + def test_content_only_on_first_message(self): + mb = MessageBuilder(color=0, timestamp="") + mb.set_content("hello world") + # Create two embeds that together exceed 6000 chars + mb.new_embed(title="T", description="x" * 5000) + mb.new_embed(title="T", description="x" * 5000) + messages = mb.build() + assert len(messages) == 2 + assert messages[0].content == "hello world" + assert messages[1].content is None + + def test_current_embed_returns_active_builder(self): + mb = MessageBuilder(color=0, timestamp="") + eb = mb.new_embed(title="T", description="D") + assert mb.current_embed is eb + + def test_large_embeds_split_across_messages(self): + mb = MessageBuilder(color=0, timestamp="") + # Create 3 embeds each ~3000 chars — can't fit 2 in one message + for i in range(3): + mb.new_embed(title=f"E{i}", description="x" * 2990) + messages = mb.build() + assert len(messages) >= 2 + for msg in messages: + combined = sum( + len(e.title or "") + len(e.description or "") + for e in msg.embeds + ) + assert combined <= 6000 + + +class TestValidateNotification: + def test_valid_notification_returns_empty(self): + notification = Notification( + username="Bot", + messages=[ + WebhookMessage( + content=None, + embeds=[ + Embed( + title="Title", + description="Desc", + color=0, + fields=[Field(name="k", value="v")], + timestamp="", + ) + ], + ) + ], + ) + assert validate_notification(notification) == [] + + def test_title_too_long(self): + notification = Notification( + username="Bot", + messages=[ + WebhookMessage( + embeds=[ + Embed( + title="x" * 257, + description="", + color=0, + fields=[], + timestamp="", + ) + ], + ) + ], + ) + violations = validate_notification(notification) + assert any("title" in v.lower() for v in violations) + + def test_field_value_too_long(self): + notification = Notification( + username="Bot", + messages=[ + WebhookMessage( + embeds=[ + Embed( + title="T", + description="", + color=0, + fields=[Field(name="k", value="x" * 1025)], + timestamp="", + ) + ], + ) + ], + ) + violations = validate_notification(notification) + assert any("field value" in v.lower() for v in violations) + + def test_too_many_fields(self): + notification = Notification( + username="Bot", + messages=[ + WebhookMessage( + embeds=[ + Embed( + title="T", + description="", + color=0, + fields=[Field(name="k", value="v") for _ in range(26)], + timestamp="", + ) + ], + ) + ], + ) + violations = validate_notification(notification) + assert any("field count" in v.lower() for v in violations) + + def test_embed_total_chars_too_large(self): + notification = Notification( + username="Bot", + messages=[ + WebhookMessage( + embeds=[ + Embed( + title="x" * 256, + description="x" * 4096, + color=0, + fields=[Field(name="k", value="x" * 1024) for _ in range(3)], + timestamp="", + ) + ], + ) + ], + ) + violations = validate_notification(notification) + assert any("embed char" in v.lower() for v in violations) + + def test_content_too_long(self): + notification = Notification( + username="Bot", + messages=[ + WebhookMessage( + content="x" * 2001, + embeds=[], + ) + ], + ) + violations = validate_notification(notification) + assert any("content" in v.lower() for v in violations) + + def test_too_many_embeds(self): + notification = Notification( + username="Bot", + messages=[ + WebhookMessage( + embeds=[ + Embed(title="T", description="", color=0, fields=[], timestamp="") + for _ in range(11) + ], + ) + ], + ) + violations = validate_notification(notification) + assert any("embed count" in v.lower() for v in violations) + + +import json +from pathlib import Path + +from notifications.formatting.canvas_format import format_notification + + +class TestIntegrationWithRealPayload: + @staticmethod + def _load_payload(): + path = Path(__file__).parent / "test-mdxcanvas-payload.json" + with open(path) as f: + return json.load(f) + + def test_success_case_passes_all_limits(self): + data = self._load_payload() + notification = format_notification( + data=data, + course_id="110", + course_name="CS 110 Course Updates", + course_url="https://byu.instructure.com/courses/20736", + author="robbykap", + author_icon="https://github.com/robbykap.png", + branch="main", + action_url="https://github.com/actions/runs/1", + ) + violations = validate_notification(notification) + assert violations == [], f"Limit violations: {violations}" + + def test_review_case_passes_all_limits(self): + data = self._load_payload() + data["content_to_review"] = [ + ("assignment", "lab-notebook-week1", "https://byu.instructure.com/courses/20736/assignments/1332409"), + ("assignment", "group-presentation", "https://byu.instructure.com/courses/20736/assignments/1332413"), + ] + notification = format_notification( + data=data, + course_id="110", + course_name="CS 110 Course Updates", + course_url="https://byu.instructure.com/courses/20736", + author="robbykap", + author_icon="https://github.com/robbykap.png", + branch="main", + action_url="https://github.com/actions/runs/1", + cicd_role_id="999888777", + ) + violations = validate_notification(notification) + assert violations == [], f"Limit violations: {violations}" + + def test_error_case_passes_all_limits(self): + data = { + "deployed_content": [], + "content_to_review": [], + "error": "SONDecodeError: Expecting value: line 1 column 1 (char 0)", + } + notification = format_notification( + data=data, + course_id="110", + course_name="CS 110 Course Updates", + course_url="https://byu.instructure.com/courses/20736", + author="robbykap", + author_icon="", + branch="main", + action_url="https://github.com/actions/runs/1", + cicd_role_id="999888777", + ) + violations = validate_notification(notification) + assert violations == [], f"Limit violations: {violations}" + + def test_overview_table_has_correct_type_count(self): + data = self._load_payload() + notification = format_notification( + data=data, + course_id="110", + course_name="CS 110 Course Updates", + course_url="https://byu.instructure.com/courses/20736", + author="robbykap", + author_icon="", + branch="main", + action_url="https://github.com/actions/runs/1", + ) + overview_field = notification.messages[0].embeds[0].fields[0] + for resource_type in ["module", "assignment", "quiz", "page"]: + assert resource_type in overview_field.value + + def test_massive_payload_passes_all_limits(self): + data = { + "deployed_content": [ + ("page", f"page-{i}", f"https://example.com/pages/{i}") + for i in range(500) + ], + "content_to_review": [], + "error": "", + } + notification = format_notification( + data=data, + course_id="110", + course_name="CS 110 Course Updates", + course_url="https://byu.instructure.com/courses/20736", + author="robbykap", + author_icon="", + branch="main", + action_url="https://github.com/actions/runs/1", + ) + violations = validate_notification(notification) + assert violations == [], f"Limit violations: {violations}" + + def test_empty_payload_has_no_content(self): + from notifications.formatting.canvas_format import has_content + data = { + "deployed_content": [], + "content_to_review": [], + "error": "", + } + assert has_content(data) is False diff --git a/tests/test_embed_chunking.py b/tests/test_embed_chunking.py new file mode 100644 index 0000000..733383d --- /dev/null +++ b/tests/test_embed_chunking.py @@ -0,0 +1,85 @@ +from notifications.discord_limits import ( + EMBED_CHAR_LIMIT, + FIELDS_PER_EMBED, + EmbedBuilder, + calc_embed_size, +) +from notifications.resources import Author, Embed, Field, Footer + + +class TestCalcEmbedSize: + def test_basic_size(self): + embed = Embed( + title="Hello", + description="World", + color=0xFF0000, + fields=[Field(name="key", value="val")], + timestamp="2025-01-01T00:00:00Z", + ) + assert calc_embed_size(embed) == len("Hello") + len("World") + len("key") + len("val") + + def test_includes_footer_and_author(self): + embed = Embed( + title="T", + description="D", + color=0, + fields=[], + timestamp="", + author=Author(name="AuthorName"), + footer=Footer(text="FooterText"), + ) + assert calc_embed_size(embed) == len("T") + len("D") + len("AuthorName") + len("FooterText") + + def test_empty_embed(self): + embed = Embed( + title="", + description="", + color=0, + fields=[], + timestamp="", + ) + assert calc_embed_size(embed) == 0 + + def test_spacer_fields(self): + embed = Embed( + title="", + description="", + color=0, + fields=[Field(name="\u200b", value="\u200b")], + timestamp="", + ) + assert calc_embed_size(embed) == 2 + + +class TestEmbedBuilderLimits: + def test_builder_never_exceeds_embed_char_limit(self): + b = EmbedBuilder( + title="Title", + description="Description text", + color=0, + timestamp="", + author=Author(name="Bot"), + footer=Footer(text="Footer"), + ) + added = 0 + for i in range(100): + if not b.add_field(f"field-{i}", "x" * 500): + break + added += 1 + embed = b.build() + assert calc_embed_size(embed) <= EMBED_CHAR_LIMIT + assert added > 0 + + def test_builder_respects_field_count_limit(self): + b = EmbedBuilder(title="T", description="D", color=0, timestamp="") + for i in range(30): + b.add_field(f"f{i}", "v") + embed = b.build() + assert len(embed.fields) <= FIELDS_PER_EMBED + + def test_all_fields_preserved_when_within_limits(self): + b = EmbedBuilder(title="T", description="D", color=0, timestamp="") + for i in range(10): + b.add_field(f"f{i}", f"v{i}") + embed = b.build() + assert len(embed.fields) == 10 diff --git a/tests/test_workflow_wiring.py b/tests/test_workflow_wiring.py new file mode 100644 index 0000000..31f41bf --- /dev/null +++ b/tests/test_workflow_wiring.py @@ -0,0 +1,48 @@ +from pathlib import Path + + +def test_send_course_notification_action_wires_course_metadata_and_short_branch(): + text = Path(".github/actions/send-course-notification/action.yml").read_text() + + assert "course-name:" in text + assert "course-url:" in text + assert '--course-name "${{ inputs.course-name }}"' in text + assert '--course-url "${{ inputs.course-url }}"' in text + assert '--branch "${{ github.ref_name }}"' in text + + +def test_course_workflows_expose_and_forward_course_metadata_inputs(): + canvas_workflow = Path(".github/workflows/mdxcanvas_automation.yaml").read_text() + docker_workflow = Path(".github/workflows/docker_automation.yaml").read_text() + + assert "course_name:" in canvas_workflow + assert "course_url:" in canvas_workflow + assert "course-name: ${{ inputs.course_name }}" in canvas_workflow + assert "course-url: ${{ inputs.course_url }}" in canvas_workflow + + assert "course_name:" in docker_workflow + assert "course_url:" in docker_workflow + assert "course-name: ${{ inputs.course_name }}" in docker_workflow + assert "course-url: ${{ inputs.course_url }}" in docker_workflow + + +def test_course_workflows_accept_utils_ref_and_use_local_utils_action(): + canvas_workflow = Path(".github/workflows/mdxcanvas_automation.yaml").read_text() + docker_workflow = Path(".github/workflows/docker_automation.yaml").read_text() + + assert "utils_ref:" in canvas_workflow + assert "utils_ref:" in docker_workflow + assert "default: main" in canvas_workflow + assert "default: main" in docker_workflow + assert "ref: ${{ inputs.utils_ref }}" in canvas_workflow + assert "ref: ${{ inputs.utils_ref }}" in docker_workflow + assert "uses: ./utils/.github/actions/send-course-notification" in canvas_workflow + assert "uses: ./utils/.github/actions/send-course-notification" in docker_workflow + + +def test_course_workflows_install_markdowndata_for_notification_formatting(): + canvas_workflow = Path(".github/workflows/mdxcanvas_automation.yaml").read_text() + docker_workflow = Path(".github/workflows/docker_automation.yaml").read_text() + + assert "pip install discord-webhook markdowndata" in canvas_workflow + assert "pip install discord-webhook markdowndata" in docker_workflow diff --git a/tests/urgent-mdxcanvas-notification.png b/tests/urgent-mdxcanvas-notification.png new file mode 100644 index 0000000..4eddeb3 Binary files /dev/null and b/tests/urgent-mdxcanvas-notification.png differ