{
  "id": "4o8ez2ydRA6Q53wX",
  "meta": {
    "instanceId": "5895d8ca92a7edd8f068d104b226176ebc0c945a1ed97c3c1dd3b247833ae825",
    "templateCredsSetupCompleted": true
  },
  "name": "02.Canvas: Send students their pending assignments",
  "tags": [],
  "nodes": [
    {
      "id": "379fa596-d650-49f7-ad08-850423b72904",
      "name": "When clicking ‘Execute workflow’",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -2048,
        128
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "77e55ac9-a267-4b88-ab11-dc15ae7bd20c",
      "name": "Initial parameters",
      "type": "n8n-nodes-base.set",
      "position": [
        -1824,
        128
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "2e7df20e-ec67-4d74-a356-e0373d287cd4",
              "name": "canvasLink",
              "type": "string",
              "value": "https://pucminas.instructure.com"
            },
            {
              "id": "209c838a-331a-49d5-a959-e960e82a31cc",
              "name": "courseName",
              "type": "string",
              "value": "25_2 - 04 - [IEC_EEP_O9_T2_Online]Int.Art.Pla."
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "7870deab-3a56-47d5-83ee-06d198d1c4e7",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1648,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 864,
        "height": 432,
        "content": "## Get course ID by course name \n"
      },
      "typeVersion": 1
    },
    {
      "id": "410e5674-7b6d-48c0-bc6e-b34912485fa2",
      "name": "Get course by name",
      "type": "n8n-nodes-base.filter",
      "position": [
        -1152,
        128
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c9eab0e6-0270-4b03-9d7a-dea4c67f809c",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.name }}",
              "rightValue": "={{ $('Initial parameters').first().json.courseName }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "0313b07c-1b68-4f99-89a2-33333a9a3f0f",
      "name": "Get all teacher courses",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        -1600,
        128
      ],
      "parameters": {
        "url": "={{ $json.canvasLink }}/api/v1/courses",
        "options": {
          "response": {
            "response": {
              "fullResponse": true
            }
          },
          "pagination": {
            "pagination": {
              "nextURL": "={{ ( $response.headers.link || '' ) .split(',') .map(p => p.trim()) .find(p => p.includes('rel=\"next\"')) ?.match(/<([^>]+)>/)?.[1] // captura a URL entre <...> || '' }}",
              "paginationMode": "responseContainsNextURL",
              "completeExpression": "={{\n  /*\n    Checks if there is NO next link in pagination\n    (no Link header or no rel=\"next\")\n  */\n  !$response.headers.link ||\n  !$response.headers.link.includes('rel=\"next\"')\n}}",
              "paginationCompleteWhen": "other"
            }
          },
          "lowercaseHeaders": true
        },
        "sendQuery": true,
        "authentication": "predefinedCredentialType",
        "queryParameters": {
          "parameters": [
            {}
          ]
        },
        "nodeCredentialType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "id": "credential-id",
          "name": "httpBearerAuth Credential"
        },
        "httpHeaderAuth": {
          "id": "credential-id",
          "name": "httpHeaderAuth Credential"
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "3c31d6f8-d5df-4b45-9836-8b9bc2f7a654",
      "name": "Get course ID",
      "type": "n8n-nodes-base.set",
      "position": [
        -928,
        128
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "e4696497-962f-4142-86fc-c1cc19717b87",
              "name": "idCurso",
              "type": "number",
              "value": "={{ $json.id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "b8418c45-d51a-46f9-8fed-8fcbf8011c92",
      "name": "Get course students",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        -704,
        128
      ],
      "parameters": {
        "url": "={{ $('Initial parameters').first().json.canvasLink }}/api/v1/courses/{{ $('Get course ID').first().json.idCurso }}/search_users",
        "options": {
          "response": {
            "response": {
              "fullResponse": true
            }
          },
          "pagination": {
            "pagination": {
              "nextURL": "={{ ( $response.headers.link || '' ) .split(',') .map(p => p.trim()) .find(p => p.includes('rel=\"next\"')) ?.match(/<([^>]+)>/)?.[1] // captura a URL entre <...> || '' }}",
              "paginationMode": "responseContainsNextURL",
              "completeExpression": "={{   /*     Checks if there is NO next link in pagination     (no Link header or no rel=\"next\")   */   !$response.headers.link ||   !$response.headers.link.includes('rel=\"next\"') }}",
              "paginationCompleteWhen": "other"
            }
          },
          "lowercaseHeaders": true
        },
        "sendQuery": true,
        "authentication": "predefinedCredentialType",
        "queryParameters": {
          "parameters": [
            {
              "name": "enrollment_type[]",
              "value": "student"
            }
          ]
        },
        "nodeCredentialType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "id": "credential-id",
          "name": "httpBearerAuth Credential"
        },
        "httpHeaderAuth": {
          "id": "credential-id",
          "name": "httpHeaderAuth Credential"
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "da6fe740-c384-487a-8ed1-c318141167de",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -304,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 432,
        "content": "## Get pending submissions by course ID and student ID\n"
      },
      "typeVersion": 1
    },
    {
      "id": "b79db951-a0b1-4e40-b5a0-49cea008c15a",
      "name": "Agregate courses pagination",
      "type": "n8n-nodes-base.code",
      "position": [
        -1376,
        128
      ],
      "parameters": {
        "language": "pythonNative",
        "pythonCode": "# n8n - Code in Python (Native)\n# Execution mode: Run Once for All Items\n#\n# Inputs:\n#   - Multiple incoming items from the previous node (available as `_items`).\n#   - Each incoming item is expected to contain:\n#       item[\"json\"][\"body\"]: a list of Canvas courses for a single pagination page.\n#\n# Purpose:\n#   - Normalize Canvas pagination by flattening multiple \"pages\" of courses into\n#     a single output stream.\n#   - Emit one n8n-compliant item per course, wrapped under the required \"json\" key.\n#\n# Output:\n#   - A list of items in the form:\n#       [{\"json\": <course_dict>}, {\"json\": <course_dict>}, ...]\n\n\n# Accumulates the flattened output (one item per course across all pages)\noutput_items = []\n\n# In n8n Native Python, all incoming items are available via `_items`\nfor item in _items:\n    # Safely extract the JSON payload from the current incoming item\n    item_data = item.get(\"json\", {}) or {}\n\n    # Retrieve the list of courses for the current page (defaults to an empty list)\n    body_courses = item_data.get(\"body\") or []\n\n    # Emit one output item per course\n    for course in body_courses:\n        # Safely coerce proxy-like objects (e.g., JsProxy) into a plain dict\n        try:\n            course_dict = dict(course)\n        except Exception:\n            # If coercion fails, keep the original object (best-effort fallback)\n            course_dict = course\n\n        # Build an n8n-compliant output item (must contain a \"json\" key)\n        output_items.append({\"json\": course_dict})\n\n# Return the flattened list: one output item per course across all received pages\nreturn output_items\n"
      },
      "typeVersion": 2
    },
    {
      "id": "2dc573cc-55b8-4104-b631-98887e3e5b43",
      "name": "Agregate students pagination",
      "type": "n8n-nodes-base.code",
      "position": [
        -480,
        128
      ],
      "parameters": {
        "language": "pythonNative",
        "pythonCode": "# n8n - Code in Python (Native)\n# Execution mode: Run Once for All Items\n#\n# Inputs:\n#   - Multiple incoming items from the previous node (available as `_items`).\n#   - Each incoming item is expected to contain:\n#       item[\"json\"][\"body\"]: a list of Canvas students for a single pagination page.\n#\n# Purpose:\n#   - Normalize Canvas pagination by flattening multiple \"pages\" of students into\n#     a single output stream.\n#   - Emit one n8n-compliant item per student, wrapped under the required \"json\" key.\n#\n# Output:\n#   - A list of items in the form:\n#       [{\"json\": <student_dict>}, {\"json\": <student_dict>}, ...]\n\n\n# Accumulates the flattened output (one item per student across all pages)\noutput_items = []\n\n# In n8n Native Python, all incoming items are available via `_items`\nfor item in _items:\n    # Safely extract the JSON payload from the current incoming item\n    item_data = item.get(\"json\", {}) or {}\n\n    # Retrieve the list of students for the current page (defaults to an empty list)\n    body_students = item_data.get(\"body\") or []\n\n    # Emit one output item per student\n    for student in body_students:\n        # Safely coerce proxy-like objects (e.g., JsProxy) into a plain dict\n        try:\n            student_dict = dict(student)\n        except Exception:\n            # If coercion fails, keep the original object (best-effort fallback)\n            student_dict = student\n\n        # Build an n8n-compliant output item (must contain a \"json\" key)\n        output_items.append({\"json\": student_dict})\n\n# Return the flattened list: one output item per student across all received pages\nreturn output_items\n"
      },
      "typeVersion": 2
    },
    {
      "id": "7dcc6b41-8ad4-49e7-af84-9804767a4374",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -752,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 432,
        "content": "## Get students by course ID \n"
      },
      "typeVersion": 1
    },
    {
      "id": "d2be3bc1-7f45-4030-8f84-d0c2311f75e3",
      "name": "Get course sumbissions",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        -256,
        200
      ],
      "parameters": {
        "url": "={{ $('Initial parameters').first().json.canvasLink }}/api/v1/courses/{{ $('Get course ID').first().json.idCurso }}//students/submissions",
        "options": {
          "response": {
            "response": {
              "fullResponse": true
            }
          },
          "pagination": {
            "pagination": {
              "nextURL": "={{ ( $response.headers.link || '' ) .split(',') .map(p => p.trim()) .find(p => p.includes('rel=\"next\"')) ?.match(/<([^>]+)>/)?.[1] // captura a URL entre <...> || '' }}",
              "paginationMode": "responseContainsNextURL",
              "completeExpression": "={{   /*     Checks if there is NO next link in pagination     (no Link header or no rel=\"next\")   */   !$response.headers.link ||   !$response.headers.link.includes('rel=\"next\"') }}",
              "paginationCompleteWhen": "other"
            }
          },
          "lowercaseHeaders": true
        },
        "sendQuery": true,
        "authentication": "predefinedCredentialType",
        "queryParameters": {
          "parameters": [
            {
              "name": "student_ids[]",
              "value": "={{ $json.id }}"
            },
            {
              "name": "include[]",
              "value": "assignment"
            },
            {
              "name": "workflow_state",
              "value": "unsubmitted"
            }
          ]
        },
        "nodeCredentialType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "id": "credential-id",
          "name": "httpBearerAuth Credential"
        },
        "httpHeaderAuth": {
          "id": "credential-id",
          "name": "httpHeaderAuth Credential"
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "5db62fe3-4e1c-4887-870c-d4b6d929a575",
      "name": "Agregate submits pagination",
      "type": "n8n-nodes-base.code",
      "position": [
        -32,
        200
      ],
      "parameters": {
        "language": "pythonNative",
        "pythonCode": "# n8n - Code in Python (Native)\n# Execution mode: Run Once for All Items\n#\n# Inputs:\n#   - Multiple incoming items from the previous node (available as `_items`).\n#   - Each incoming item is expected to contain:\n#       item[\"json\"][\"body\"]: a list of Canvas submissions for a single pagination page.\n#\n# Purpose:\n#   - Normalize Canvas pagination by flattening multiple \"pages\" of submissions into\n#     a single output stream.\n#   - Emit one n8n-compliant item per submission, wrapped under the required \"json\" key.\n#\n# Output:\n#   - A list of items in the form:\n#       [{\"json\": <submit_dict>}, {\"json\": <submit_dict>}, ...]\n\n\n# Accumulates the flattened output (one item per submit across all pages)\noutput_items = []\n\n# In n8n Native Python, all incoming items are available via `_items`\nfor item in _items:\n    # Safely extract the JSON payload from the current incoming item\n    item_data = item.get(\"json\", {}) or {}\n\n    # Retrieve the list of submits for the current page (defaults to an empty list)\n    body_submits = item_data.get(\"body\") or []\n\n    # Emit one output item per submit\n    for submit in body_submits:\n        # Safely coerce proxy-like objects (e.g., JsProxy) into a plain dict\n        try:\n            submit_dict = dict(submit)\n        except Exception:\n            # If coercion fails, keep the original object (best-effort fallback)\n            submit_dict = submit\n\n        # Build an n8n-compliant output item (must contain a \"json\" key)\n        output_items.append({\"json\": submit_dict})\n\n# Return the flattened list: one output item per submit across all received pages\nreturn output_items\n"
      },
      "typeVersion": 2
    },
    {
      "id": "71ff92d4-67d9-41a9-8a6c-c0671889ba88",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1872,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 192,
        "height": 432,
        "content": "## Set course name"
      },
      "typeVersion": 1
    },
    {
      "id": "8485feb1-1e07-44a0-882f-8ef5fb252a66",
      "name": "Enrich Submissions with Student Data",
      "type": "n8n-nodes-base.code",
      "position": [
        416,
        128
      ],
      "parameters": {
        "language": "pythonNative",
        "pythonCode": "# n8n - Code in Python (Native)\n# Execution mode: Run Once for All Items\n#\n# Inputs (IMPORTANT):\n#   - This node expects the output of a Merge node configured as \"Append\".\n#   - The Merge node must append two independent streams into a single list:\n#       1) Student records from \"Agregate students pagination\"\n#       2) Submission records from \"Agregate submits pagination\"\n#\n# Purpose:\n#   - Split the appended incoming items into two groups (students vs. submissions).\n#   - Build a lookup map (student_id -> student record).\n#   - Enrich each submission with student identity fields plus key assignment metadata.\n#   - Convert \"assignment.due_at\" to São Paulo time (UTC-03:00) and format as DD/MM/YYYY HH:MM:SS.\n#\n# Output:\n#   - One output item per submission:\n#       {\"json\": {student fields + assignment fields + submission fields}}\n\n\ndef _to_key(value):\n    \"\"\"\n    Normalize IDs to comparable string keys.\n\n    Canvas IDs may arrive as int, float-like, or string. This helper ensures we\n    can reliably match student.id with submission.user_id.\n    \"\"\"\n    if value is None:\n        return None\n\n    try:\n        return str(int(value))\n    except Exception:\n        text = str(value).strip()\n        return text or None\n\n\n# -----------------------------------------------------------------------------\n# Date helpers (no imports) - ISO 8601 -> São Paulo (UTC-03:00) formatter\n# -----------------------------------------------------------------------------\n\ndef _is_leap_year(year):\n    \"\"\"Return True if year is a leap year in the Gregorian calendar.\"\"\"\n    if (year % 400) == 0:\n        return True\n    if (year % 100) == 0:\n        return False\n    return (year % 4) == 0\n\n\ndef _days_in_month(year, month):\n    \"\"\"Return the number of days in a given month.\"\"\"\n    if month in (1, 3, 5, 7, 8, 10, 12):\n        return 31\n    if month in (4, 6, 9, 11):\n        return 30\n    # February\n    return 29 if _is_leap_year(year) else 28\n\n\ndef _increment_day(year, month, day):\n    \"\"\"Add one day to a YYYY-MM-DD date.\"\"\"\n    dim = _days_in_month(year, month)\n    day += 1\n    if day > dim:\n        day = 1\n        month += 1\n        if month > 12:\n            month = 1\n            year += 1\n    return year, month, day\n\n\ndef _decrement_day(year, month, day):\n    \"\"\"Subtract one day from a YYYY-MM-DD date.\"\"\"\n    day -= 1\n    if day < 1:\n        month -= 1\n        if month < 1:\n            month = 12\n            year -= 1\n        day = _days_in_month(year, month)\n    return year, month, day\n\n\ndef _parse_iso8601(dt_text):\n    \"\"\"\n    Parse an ISO 8601 datetime string into components:\n      (year, month, day, hour, minute, second, offset_minutes)\n\n    Supported examples:\n      - 2025-12-19T02:59:59Z\n      - 2025-12-19T02:59:59.123Z\n      - 2025-12-19T02:59:59+00:00\n      - 2025-12-19T02:59:59-03:00\n\n    Returns None if parsing fails.\n    \"\"\"\n    if not dt_text:\n        return None\n\n    text = str(dt_text).strip()\n    if not text:\n        return None\n\n    offset_minutes = None\n\n    # Handle trailing 'Z' (UTC)\n    if text.endswith(\"Z\"):\n        offset_minutes = 0\n        text = text[:-1]\n\n    # Handle explicit timezone offsets ±HH:MM (only if not already 'Z')\n    if offset_minutes is None and len(text) >= 6:\n        tail = text[-6:]\n        sign = tail[0]\n        if sign in (\"+\", \"-\") and tail[3] == \":\":\n            try:\n                off_h = int(tail[1:3])\n                off_m = int(tail[4:6])\n                offset_minutes = off_h * 60 + off_m\n                if sign == \"-\":\n                    offset_minutes = -offset_minutes\n                text = text[:-6]\n            except Exception:\n                return None\n\n    # Default: if no timezone specified, treat as UTC\n    if offset_minutes is None:\n        offset_minutes = 0\n\n    # Split date and time\n    if \"T\" in text:\n        date_part, time_part = text.split(\"T\", 1)\n    elif \" \" in text:\n        date_part, time_part = text.split(\" \", 1)\n    else:\n        return None\n\n    # Drop fractional seconds if present\n    if \".\" in time_part:\n        time_part = time_part.split(\".\", 1)[0]\n\n    try:\n        y_str, mo_str, d_str = date_part.split(\"-\", 2)\n        hh_str, mm_str, ss_str = time_part.split(\":\", 2)\n\n        year = int(y_str)\n        month = int(mo_str)\n        day = int(d_str)\n        hour = int(hh_str)\n        minute = int(mm_str)\n        second = int(ss_str)\n\n        return year, month, day, hour, minute, second, offset_minutes\n    except Exception:\n        return None\n\n\ndef _shift_by_minutes(year, month, day, hour, minute, second, delta_minutes):\n    \"\"\"Shift a datetime by delta_minutes, normalizing date rollover (no imports).\"\"\"\n    total_seconds = (hour * 3600) + (minute * 60) + second\n    total_seconds += int(delta_minutes) * 60\n\n    # Normalize seconds across day boundaries\n    while total_seconds < 0:\n        total_seconds += 86400\n        year, month, day = _decrement_day(year, month, day)\n\n    while total_seconds >= 86400:\n        total_seconds -= 86400\n        year, month, day = _increment_day(year, month, day)\n\n    hour = total_seconds // 3600\n    rem = total_seconds % 3600\n    minute = rem // 60\n    second = rem % 60\n\n    return year, month, day, hour, minute, second\n\n\ndef format_due_sao_paulo(due_at):\n    \"\"\"\n    Convert a Canvas ISO 8601 datetime string to São Paulo local time (UTC-03:00),\n    returning a string formatted as 'DD/MM/YYYY HH:MM:SS'.\n\n    Notes:\n      - This uses a fixed UTC-03:00 offset (GMT-3).\n      - If parsing fails, returns the original input.\n    \"\"\"\n    if not due_at:\n        return \"\"\n\n    parsed = _parse_iso8601(due_at)\n    if not parsed:\n        return due_at\n\n    year, month, day, hour, minute, second, offset_minutes = parsed\n\n    # Convert \"local time at offset\" -> UTC -> São Paulo (UTC-03:00)\n    # UTC = local - offset_minutes\n    # SP  = UTC  - 180 minutes\n    delta_minutes = (-offset_minutes) - 180\n\n    year, month, day, hour, minute, second = _shift_by_minutes(\n        year, month, day, hour, minute, second, delta_minutes\n    )\n\n    dd = str(day).zfill(2)\n    mm = str(month).zfill(2)\n    yyyy = str(year).zfill(4)\n    hh = str(hour).zfill(2)\n    mi = str(minute).zfill(2)\n    ss = str(second).zfill(2)\n\n    return f\"{dd}/{mm}/{yyyy} {hh}:{mi}:{ss}\"\n\n\n# -----------------------------------------------------------------------------\n# 1) Split appended incoming items into: students and submissions\n# -----------------------------------------------------------------------------\n\nstudents_map = {}\nsubmissions = []\n\nfor item in _items:\n    payload = item.get(\"json\", {}) or {}\n\n    # Heuristic classification:\n    # - Submissions typically contain \"user_id\" and an \"assignment\" object.\n    # - Students typically contain \"id\" plus identity fields like \"login_id\",\n    #   \"sis_user_id\", etc.\n    is_submission = (\"user_id\" in payload) and (\"assignment\" in payload)\n\n    if is_submission:\n        submissions.append(payload)\n        continue\n\n    # Treat as a student record when it has an identifiable \"id\"\n    student_id_raw = payload.get(\"id\")\n    student_key = _to_key(student_id_raw)\n\n    if student_key is not None:\n        students_map[student_key] = payload\n\n\n# -----------------------------------------------------------------------------\n# 2) Enrich submissions using the student lookup map + format due_at (SP time)\n# -----------------------------------------------------------------------------\n\noutput_items = []\n\nfor submission in submissions:\n    assignment = submission.get(\"assignment\") or {}\n\n    student_id_raw = submission.get(\"user_id\")\n    student_key = _to_key(student_id_raw)\n\n    student_data = students_map.get(student_key, {}) or {}\n\n    due_at_raw = assignment.get(\"due_at\")\n    due_at_sp = format_due_sao_paulo(due_at_raw)\n\n    output_items.append({\n        \"json\": {\n            # Student identity\n            \"student_id\": student_id_raw,\n            \"student_name\": student_data.get(\"name\"),\n            \"sis_user_id\": student_data.get(\"sis_user_id\"),\n            \"login_id\": student_data.get(\"login_id\"),\n            \"email\": student_data.get(\"email\"),\n\n            # Assignment identity\n            \"assignment_id\": assignment.get(\"id\"),\n            \"assignment_name\": assignment.get(\"name\"),\n\n            # Due date/time (converted to São Paulo time and formatted)\n            \"due_at\": due_at_sp,\n\n            # Submission / assignment metadata\n            \"workflow_state\": submission.get(\"workflow_state\"),\n            \"html_url\": assignment.get(\"html_url\"),\n        }\n    })\n\nreturn output_items\n"
      },
      "typeVersion": 2
    },
    {
      "id": "07a70c5e-5d78-48c3-b7ae-daf09f94b7be",
      "name": "Group Pending Assignments per Student",
      "type": "n8n-nodes-base.code",
      "position": [
        640,
        128
      ],
      "parameters": {
        "language": "pythonNative",
        "pythonCode": "# n8n - Code in Python (Native)\n# Execution mode: Run Once for All Items\n#\n# Inputs:\n#   - Multiple incoming items from the previous node (available as `_items`).\n#   - Each item[\"json\"] is expected to contain (one pending assignment per item):\n#       student_id, student_name, sis_user_id, login_id, email,\n#       assignment_id, assignment_name, due_at, workflow_state, html_url\n#\n# Purpose:\n#   - Group pending assignments by student_id.\n#   - Build a consolidated, human-readable text summary per student.\n#   - Emit one output item per student, in n8n-compliant format.\n#\n#\n# Output:\n#   - A list of items in the form:\n#       [{\"json\": {student fields + pending_count + pending_summary}}, ...]\n\n\n# Accumulates all output items to be returned by this node\noutput_items = []\n\n# In n8n Native Python, all incoming items are available via `_items`\ninput_items = _items\n\n\n# -----------------------------------------------------------------------------\n# 1) Group pending assignments by student_id\n# -----------------------------------------------------------------------------\n\ngrouped_by_student = {}\n\nfor item in input_items:\n    # Safely extract the JSON payload from the current incoming item\n    item_data = item.get(\"json\", {}) or {}\n\n    # Student identity fields (used both for grouping and message personalization)\n    student_id = item_data.get(\"student_id\")\n    student_name = item_data.get(\"student_name\")\n    sis_user_id = item_data.get(\"sis_user_id\")\n    login_id = item_data.get(\"login_id\")\n    email = item_data.get(\"email\")\n\n    # Assignment fields (one \"pending assignment\" record per incoming item)\n    assignment_id = item_data.get(\"assignment_id\")\n    assignment_name = item_data.get(\"assignment_name\")\n    due_at = item_data.get(\"due_at\")\n    workflow_state = item_data.get(\"workflow_state\")\n    html_url = item_data.get(\"html_url\")\n\n    # Initialize the student group on first occurrence\n    if student_id not in grouped_by_student:\n        grouped_by_student[student_id] = {\n            \"student_id\": student_id,\n            \"student_name\": student_name,\n            \"sis_user_id\": sis_user_id,\n            \"login_id\": login_id,\n            \"email\": email,\n            \"pending_assignments\": [],\n        }\n\n    # Append the current pending assignment to the student's list\n    grouped_by_student[student_id][\"pending_assignments\"].append(\n        {\n            \"assignment_id\": assignment_id,\n            \"assignment_name\": assignment_name,\n            \"due_at\": due_at,\n            \"workflow_state\": workflow_state,\n            \"html_url\": html_url,\n        }\n    )\n\n\n# -----------------------------------------------------------------------------\n# 2) Build consolidated text summary per student and emit output items\n# -----------------------------------------------------------------------------\n\nfor student in grouped_by_student.values():\n    lines = []\n\n    student_id = student.get(\"student_id\")\n    student_name = (student.get(\"student_name\") or \"\").strip()\n    sis_user_id = student.get(\"sis_user_id\")\n    login_id = student.get(\"login_id\")\n    email = student.get(\"email\")\n    pending_assignments = student.get(\"pending_assignments\", [])\n\n    # Header line with student identification (personalized greeting)\n    if student_name:\n        greeting = f\"Olá, {student_name}, tudo bem?\\n\\n\"\n    else:\n        # Best-effort fallback when the student's name is missing upstream\n        greeting = \"Olá, tudo bem?\\n\\n\"\n\n    lines.append(\n        greeting\n        + \"Consta no Canvas da PUC Minas que você ainda não realizou ou entregou \"\n        + \"as seguintes atividades:\\n\\n\"\n    )\n\n    # Handle case with no pending assignments for the student\n    if not pending_assignments:\n        lines.append(\"- Nenhuma pendência encontrada.\")\n    else:\n        # Detail each pending assignment in a numbered list\n        for index, assignment in enumerate(pending_assignments, start=1):\n            assignment_name = assignment.get(\"assignment_name\") or \"Sem título\"\n            due = assignment.get(\"due_at\") or \"sem data final\"\n            html_url = assignment.get(\"html_url\") or \"sem link\"\n\n            lines.append(\n                f\"{index}) {assignment_name}\\n\"\n                f\"- Data limite para entrega: {due}\\n\"\n                f\"- Link: {html_url}\\n\\n\"\n            )\n\n    # Join all lines into a single multi-line string\n    pending_summary_text = \"\\n\".join(lines)\n\n    # Build an n8n-compliant output item (must contain a \"json\" key)\n    output_items.append(\n        {\n            \"json\": {\n                \"student_id\": student_id,\n                \"student_name\": student_name or None,\n                \"sis_user_id\": sis_user_id,\n                \"login_id\": login_id,\n                \"email\": email,\n                \"pending_count\": len(pending_assignments),\n                \"pending_summary\": pending_summary_text,\n            }\n        }\n    )\n\n# Return one item per student with the aggregated summary\nreturn output_items\n"
      },
      "typeVersion": 2
    },
    {
      "id": "aea4d976-c956-46a1-b5cc-c6e4e0901286",
      "name": "Send message in Canvas",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        864,
        128
      ],
      "parameters": {
        "url": "={{ $('Initial parameters').first().json.canvasLink }}/api/v1/conversations",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "form-urlencoded",
        "sendHeaders": true,
        "authentication": "predefinedCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "recipients[]",
              "value": "={{ $json.student_id }}"
            },
            {
              "name": "subject",
              "value": "={{ $('Get course by name').first().json.original_name }} - Atividades pendentes"
            },
            {
              "name": "body",
              "value": "={{ $json.pending_summary }}"
            },
            {
              "name": "force_new",
              "value": "1"
            },
            {
              "name": "context_code",
              "value": "={{ 'course_' + $('Get course by name').first().json.id }}"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {}
          ]
        },
        "nodeCredentialType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "id": "credential-id",
          "name": "httpBearerAuth Credential"
        },
        "httpHeaderAuth": {
          "id": "credential-id",
          "name": "httpHeaderAuth Credential"
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "cb6d7e0f-3fa6-438f-9a48-9a0a4a6ee358",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        144,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 432,
        "content": "## Organize and format pending submissions information"
      },
      "typeVersion": 1
    },
    {
      "id": "1c93d3c3-5093-4055-91ef-d1e417918f9d",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        816,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 208,
        "height": 432,
        "content": "## Send pending submissions alert to students"
      },
      "typeVersion": 1
    },
    {
      "id": "8f96622e-fa61-4ebe-9115-4ac260225859",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2944,
        -64
      ],
      "parameters": {
        "width": 800,
        "height": 448,
        "content": "## Canvas: Send students their pending assignments\n\n### How it works\n1. Trigger the workflow and set the Canvas base URL and target course name.\n2. Fetch all instructor courses and locate the course ID that matches the name.\n3. Retrieve enrolled students and their unsubmitted submissions for the course, handling paginated results.\n4. Merge student records with submission data, convert due dates to local time, and build a per-student summary.\n5. Send a Canvas conversation to each student with a personalized list of pending assignments and links.\n\n### Setup\n- [ ] Connect Canvas API credentials (Bearer and header auth used by the workflow).\n- [ ] Enter your Canvas base URL (e.g. https://your_educational_institution.instructure.com).\n- [ ] Set the exact course name to check for pending work.\n- [ ] Confirm the teacher account can view students and send conversations.\n- [ ] Run the workflow manually to verify output and delivery.\n- [ ] Edit the message subject or body template if you need different wording.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "6e1cf236-d4ed-4a2b-93b0-c1b5ed6fdd62",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        192,
        128
      ],
      "parameters": {},
      "typeVersion": 3.2
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "2d903e55-3229-43d1-8fd6-11630e216b74",
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "Enrich Submissions with Student Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get course ID": {
      "main": [
        [
          {
            "node": "Get course students",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get course by name": {
      "main": [
        [
          {
            "node": "Get course ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Initial parameters": {
      "main": [
        [
          {
            "node": "Get all teacher courses",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get course students": {
      "main": [
        [
          {
            "node": "Agregate students pagination",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get course sumbissions": {
      "main": [
        [
          {
            "node": "Agregate submits pagination",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get all teacher courses": {
      "main": [
        [
          {
            "node": "Agregate courses pagination",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Agregate courses pagination": {
      "main": [
        [
          {
            "node": "Get course by name",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Agregate submits pagination": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Agregate students pagination": {
      "main": [
        [
          {
            "node": "Get course sumbissions",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Enrich Submissions with Student Data": {
      "main": [
        [
          {
            "node": "Group Pending Assignments per Student",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking ‘Execute workflow’": {
      "main": [
        [
          {
            "node": "Initial parameters",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Group Pending Assignments per Student": {
      "main": [
        [
          {
            "node": "Send message in Canvas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}