FS-Canvas-Overføring (LMS) - Not reliable, under construction.

FS-Canvas-Overføring (LMS) - Not reliable, under construction.

 

Innledning

Dette er en integrasjon for overføring av data fra FS til Canvas. Integrasjonen består av 7 hoved deler.

overviewDiagram.png

Dette er en typisk synk-integrasjon der en del data fra FS skal kopieres over til Canvas og holdes i synk basert på hendelser.

Nøkkel info

Initiesering av flyt

scheduler og hendelse basert

Kjører daglig også når skjer endring på dataene i FS

Flyt møsnter

Synkron

 

Bruk av meldingskø

Nei

 

Open API

Nei

 

IntArk

Ikke brukt

 

Bakgrunn

LMS trenger en nye datasoverføringsintegrasjon som studie-strukturen (institusjon, fakultet, institutt, gruppe, emner, studieprogram, kull, klasse, studenter, lærerer og administratorer) er synkronisert slikt at dataene speils FS dataene til Canvas systemet. Denne informasjon har sin kilde i FS.

For kravspek/bestillings-dokument, se her :

Interessenter

Dette er en leveranse til tjenesten utdanningstjenesten.

Brukerhistorie / Integrasjonens omfang (arbeidet som skal gjøres)

 

  • Daglig skal det kjøres en synk som vil sørge for alle dataene nevnt ovenfor.

  • Daglig skal det sjekkes for eventuelle endringer i dataene nevnt ovenfor og utføre dem. F.eks om en studieprogram er flyttet.

  • Daglig sjekke og oppdatere rolle-tilknyttinger på emne og studieprogram nivå.

Systemer/tjenester

  • Detaljert liste av alle innvolverte systemer/tjenester Hva utveksler data? Fra hvor / Til hvor?

 

Tilgangsstyring og logging

  • Integrasjonen loger til Humio med detaljert logging av prosessen.

  • Integrasjonen har ikke noe behov for tilgangstyring

Behandlingstid/responstid og volum

  • Integrasjonen ikke tatt til bruker nå; ingen data!

Diagram som visualizerer emneUEUA flyten

Diagrammet viser kun de visualiserbare delene av emneUEUAflyten (uten flertermin delen som vil legges til) og det er analysering og koding bak nesten alle elementer og transformasjoner som ikke kan vises.

Flertermin er under arbeid og vil sannsynligvis være den mest kompliserte delen av emneflyten.

Vi har 6 slike flyter (1. oppretter konto hierarki 2. kobler emner til kontoer 3. kobler evuKurs til emneKontoer 4. kobler UEUAer til emner 5. kobler studieprogramer/kull/klasse til kontoer 6. melder brukere til emner, UEUA, evuKurs, studieprogram, kull, og klasse).

flowchart TD classDef flow fill:#ecfdf5,stroke:#047857,stroke-width:2px,color:#065f46 classDef step fill:#f1f5f9,stroke:#475569,stroke-width:1px,color:#0f172a classDef ext fill:#fef3c7,stroke:#d97706,color:#92400e classDef decision fill:#fff,stroke:#64748b,stroke-dasharray:3 3 classDef loop fill:#eff6ff,stroke:#3b82f6,color:#1e3a8a classDef note fill:#f5f3ff,stroke:#6d28d9,color:#4c1d95 classDef red fill:#fee2e2,stroke:#b91c1c,stroke-width:2px,color:#7f1d1d classDef blue fill:#dbeafe,stroke:#1e40af,stroke-width:2px,color:#1e3a8a classDef canvasRed fill:#fecaca,stroke:#b91c1c,stroke-width:2px,color:#7f1d1d %% Note nodes F1note1["Wait for Konto flow to finish"]:::note F3note1["Skip emne (no Canvas account)"]:::note F3note2["Skip parent UE handling"]:::note F3note3["Child fleretermin not supported"]:::note F6note1["Skip child UE (no parent course)"]:::note %% listOfChanges-UEUA2Emne-FLOW-scheduler subgraph F0["listOfChanges-UEUA2Emne-FLOW-scheduler"] direction TB F00s1["Manual trigger"]:::step F0s1["Scheduler trigger"]:::step F0s2["Choose Run type: 1- Changed data set 2- Full data set"]:::step F0s3["Set targetFlow"]:::step F0s4[["Call linkUEUA2EmneFromFS-CourseSectionType-FLOW"]]:::ext F0s1 --> F0s2 --> F0s3 --> F0s4 F00s1 --> F0s2 end %% linkUEUA2EmneFromFS-CourseSectionType-FLOW subgraph F1["linkUEUA2EmneFromFS-CourseSectionType-FLOW"] direction TB F1s1["Set appVersion"]:::step F1s2[("Retrieve emne2KontoFlowCompleted")]:::ext F1s3["Force emne2KontoFlowCompleted=true"]:::step F1d1{emne2KontoFlowCompleted?}:::decision F1s4[["Call getOrgListWithActiveFsLms-subFlow"]]:::ext F1s5["Filter org list"]:::step F1l1(("For Each Org")):::loop F1s6[["Call forEachOrg-UEUA2Emne-subFLOW"]]:::ext F1s7[("Store emne2KontoFlowCompleted=false")]:::ext F1s1 --> F1s2 --> F1s3 --> F1d1 F1d1 -->|"true"| F1s4 --> F1s5 --> F1l1 --> F1s6 --> F1s7 F1d1 -->|"false"| F1note1 end %% forEachOrg-UEUA2Emne-subFLOW subgraph F2["forEachOrg-UEUA2Emne-subFLOW"] direction TB F2s1[["Call extractOrgInfo.FSAccessInfo.CanvasAccessInfo-subFLOW"]]:::ext F2s2["Set FsLmsTransferInfo variables"]:::step F2s3["Compute syncPeriodList"]:::step F2l1(("For Each SyncPeriod")):::loop F2s4["Set syncPeriod"]:::step F2s5["Set endCursorName"]:::step F2s6["Clear auditInfoMap"]:::flow F2s7["Init auditInfoMap"]:::flow F2s8[("Retrieve previousCursorEnd")]:::ext F2t1["Build FS query variables and expression"]:::step F2s9[["Call fsQueryExecution-subFLOW"]]:::ext F2s10["Store filtered fs2JsonOutput"]:::step F2s11["Set currentCursorEnd"]:::step F2s12[("Store end cursor")]:::ext F2s13["Update audit info map"]:::flow F2s14["Transform to emneList"]:::step F2s15[["Call logArrayNameSize"]]:::ext F2l2(("For Each Emne")):::loop F2s16[["Call forEachEmne-subFLOW"]]:::ext F2s17[["Call auditLogRegister-subFLOW"]]:::ext F2s1 --> F2s2 --> F2s3 --> F2l1 F2l1 --> F2s4 --> F2s5 --> F2s6 --> F2s7 --> F2s8 --> F2t1 --> F2s9 --> F2s10 --> F2s11 --> F2s12 --> F2s13 --> F2s14 --> F2s15 --> F2l2 --> F2s16 --> F2s17 end %% forEachEmne-subFLOW subgraph F3["forEachEmne-subFLOW"] direction TB F3s1["Set emne variables"]:::step F3s2["Filter sprakvalg"]:::step F3s3["Set sis_account_id"]:::step F3s4["Update audit info (emne)"]:::flow F3s5[["Call getCanvasAccount-by-sis_account_id-subFLOW"]]:::ext F3s6["Cache Canvas account flags"]:::step F3d1{Account exists?}:::decision F3s7["Prepare parent UE list"]:::step F3d2{Parent UE list empty?}:::decision F3d3{Flere termin?}:::decision F3s8["Aggregate UA nodes"]:::step F3s9[["Call ForEachUE-linkUEUA2Emne-subFLOW"]]:::ext F3s10(("Loop parent UE")):::loop F3s11[["Call ForEachUEChild-linkUEUA2Emne-subFLOW"]]:::ext F3s12["Prepare child UE list"]:::step F3d4{Child fleretermin?}:::decision F3s13(("Loop child UE")):::loop F3s14["Set emneID"]:::step F3s15[["Call getAndRegisterFSBrukerInUEUA-subFLOW"]]:::ext F3s1 --> F3s2 --> F3s3 --> F3s4 --> F3s5 --> F3s6 --> F3d1 F3d1 -->|"no"| F3note1 F3d1 -->|"yes"| F3s7 --> F3d2 F3d2 -->|"empty"| F3note2 F3d2 -->|"has UE"| F3d3 F3d3 -->|"true"| F3s8 --> F3s9 F3d3 -->|"false"| F3s10 --> F3s9 F3s7 --> F3s12 --> F3d4 F3d4 -->|"true"| F3note3 F3d4 -->|"false"| F3s13 --> F3s11 F3s6 --> F3s14 --> F3s15 end %% ForEachUE-linkUEUA2Emne-subFLOW subgraph F4["ForEachUE-linkUEUA2Emne-subFLOW"] direction TB F4s1["Update audit info (UE)"]:::flow F4s2["Set UE context"]:::step F4s3["Clear UE scoped variables"]:::step F4s4["Set term and sprak"]:::step F4d1{UE has LMS rom?}:::decision F4s5["Prepare term ids"]:::step F4s6[["Call createUpdateEnrollmentTerm-subFLOW"]]:::ext F4s7["Prepare Canvas course variables"]:::step F4s8[["Call UE[CourseType]-createUpdateCourse-subFLOW"]]:::ext F4s9["Prepare Canvas section variables"]:::step F4s10[["Call UE[SectionType]-createUpdateSection-subFLOW"]]:::ext F4s11["Prepare manual section variables"]:::step F4s12[["Call UE[SectionType]-createUpdateSection-subFLOW"]]:::ext F4s13["Group UA nodes"]:::step F4l1(("For Each UA group")):::loop F4s14[["Call forEachUAGroup-subFLOW"]]:::ext F4s1 --> F4s2 --> F4s3 --> F4s4 --> F4d1 F4d1 -->|"yes"| F4s5 --> F4s6 --> F4s7 --> F4s8 --> F4s9 --> F4s10 --> F4s11 --> F4s12 F4d1 -->|"no"| F4s13 F4s12 --> F4s13 --> F4l1 --> F4s14 end %% forEachUAGroup-subFLOW subgraph F5["forEachUAGroup-subFLOW"] direction TB F5s1["Update audit info (UA group)"]:::flow F5s2["Set UA list context"]:::step F5s3["Check LMS flags"]:::step F5s4["Prepare term ids"]:::step F5s5[["Call createUpdateEnrollmentTerm-subFLOW"]]:::ext F5s6["Prepare parent account"]:::step F5d1{"What to create?"}:::decision F5s7["Prepare section variables"]:::step F5s8[["Call UA[SectionType]-createUpdateSection-subFLOW"]]:::ext F5s9["Prepare course variables"]:::step F5s10[["Call UA[CourseType]-createUpdateCourse-subFLOW"]]:::ext F5s11["Prepare manual section"]:::step F5s12[["Call UA[SectionType]-createUpdateSection-subFLOW"]]:::ext F5s1 --> F5s2 --> F5s3 --> F5s4 --> F5s5 --> F5s6 --> F5d1 F5d1 -->|"section only"| F5s7 --> F5s8 F5d1 -->|"course + section"| F5s9 --> F5s10 --> F5s11 --> F5s12 F5d1 -->|"course as section"| F5s7 --> F5s8 --> F5s11 --> F5s12 end %% ForEachUEChild-linkUEUA2Emne-subFLOW subgraph F6["ForEachUEChild-linkUEUA2Emne-subFLOW"] direction TB F6s1["Update audit info (child UE)"]:::flow F6s2["Set parentEmneKode"]:::step F6t1["Build parent FS query"]:::step F6s3[["Call fsQueryExecution-subFLOW"]]:::ext F6s4["Extract parentEmneInfo"]:::step F6s5["Set parent course SIS id"]:::step F6s6[["Call getCourse-by-sis_course_id-subFLOW"]]:::ext F6s7["Cache Canvas course flags"]:::step F6d1{Parent course exists?}:::decision F6s8["Prepare child section"]:::step F6s9[["Call UE[SectionType]-createUpdateSection-subFLOW"]]:::ext F6s10["Group UA nodes"]:::step F6l1(("For Each UA group")):::loop F6s11[["Call forEachUAGroup-subFLOW"]]:::ext F6s1 --> F6s2 --> F6t1 --> F6s3 --> F6s4 --> F6s5 --> F6s6 --> F6s7 --> F6d1 F6d1 -->|"yes"| F6s8 --> F6s9 F6d1 -->|"no"| F6note1 F6s7 --> F6s10 --> F6l1 --> F6s11 end %% getAndRegisterFSBrukerInUEUA-subFLOW subgraph F7["getAndRegisterFSBrukerInUEUA-subFLOW"] direction TB F7s1["Update audit info (users)"]:::flow F7s2["Init brukerErrorList"]:::step F7t1["Build soknad FS query"]:::step F7s3[["Call fsQueryExecution-subFLOW"]]:::ext F7s4["Transform emneSoknaderList"]:::step F7s5[["Call logArrayNameSize"]]:::ext F7s6["Transform emneUEUASoknaderList"]:::step F7l1(("For Each soknad")):::loop F7s7["Set soknad and person data"]:::step F7s8["Compute SIS user IDs"]:::step F7s9["Collect UE list and UA SIS ids"]:::step F7s10[["Call collectUAPartiPlasseringWithFSID-subFLOW"]]:::ext F7s11[["Call ForEachUEinUEList-linkUEUA2Emne-subFLOW"]]:::ext F7s12["Merge UEUASoknaderList"]:::step F7t2["Build roller FS query (emne)"]:::step F7s13[["Call fsQueryExecution-subFLOW"]]:::ext F7s14["Transform emneRollerList"]:::step F7s15[["Call logArrayNameSize"]]:::ext F7s16["Transform emneUEUARollerList"]:::step F7l2(("For Each rolle in emne")):::loop F7s17[["Call ForEachUERolleInEmneUERollerList-subFLOW"]]:::ext F7t3["Build roller FS query (UE)"]:::step F7s18[["Call fsQueryExecution-subFLOW"]]:::ext F7s19["Transform UERollerList"]:::step F7s20[["Call logArrayNameSize"]]:::ext F7s21["Transform UEFagRollerList"]:::step F7l3(("For Each rolle in UE")):::loop F7s22[["Call ForEachUERolleInUERollerList-subFLOW"]]:::ext F7t4["Build roller FS query (timeplan)"]:::step F7s23[["Call fsQueryExecution-subFLOW"]]:::ext F7s24["Transform timePlanRollerList"]:::step F7t5["Build roller FS query (UA)"]:::step F7s25[["Call fsQueryExecution-subFLOW"]]:::ext F7s26["Transform UARollerList"]:::step F7s27["Transform timePlanUARolleList"]:::step F7s28[["Call logArrayNameSize"]]:::ext F7s29["Transform UAFagRollerList"]:::step F7l4(("For Each rolle in UA")):::loop F7s30[["Call ForEachUARolleInUARollerList-subFLOW"]]:::ext F7s31["Summarise enrollment inputs"]:::step F7l5(("For Each UE or UA enrollment")):::loop F7s32[["Call deleteCreateEnrollment-subFLOW"]]:::ext F7s33["Set auditInfoMapKey"]:::flow F7s34["Validate brukerErrorList empty"]:::step F7s1 --> F7s2 --> F7t1 --> F7s3 --> F7s4 --> F7s5 --> F7s6 --> F7l1 F7l1 --> F7s7 --> F7s8 --> F7s9 --> F7s10 --> F7s11 --> F7s12 --> F7t2 F7t2 --> F7s13 --> F7s14 --> F7s15 --> F7s16 --> F7l2 --> F7s17 --> F7t3 --> F7s18 --> F7s19 --> F7s20 --> F7s21 --> F7l3 --> F7s22 --> F7t4 --> F7s23 --> F7s24 --> F7t5 --> F7s25 --> F7s26 --> F7s27 --> F7s28 --> F7s29 --> F7l4 --> F7s30 --> F7s31 --> F7l5 --> F7s32 --> F7s33 --> F7s34 end %% collectUAPartiPlasseringWithFSID-subFLOW subgraph F8["collectUAPartiPlasseringWithFSID-subFLOW"] direction TB F8s1["Clear parti variables"]:::step F8t1["Build parti FS query"]:::step F8s2[["Call fsQueryExecution-subFLOW"]]:::ext F8s3["Transform undervisningspartiplasseringerList"]:::step F8s4["Transform UAListInPartiPlassering"]:::step F8s1 --> F8t1 --> F8s2 --> F8s3 --> F8s4 end %% ForEachUEinUEList-linkUEUA2Emne-subFLOW subgraph F9["ForEachUEinUEList-linkUEUA2Emne-subFLOW"] direction TB F9s1["Set UE"]:::step F9s2["Clear UE enrollment variables"]:::step F9s3["Set course_id and SIS ids"]:::step F9s4["Transform emneUEUASoknaderList"]:::step F9l1(("For Each UA")):::loop F9s5["Append UA enrollment entries"]:::step F9s1 --> F9s2 --> F9s3 --> F9s4 --> F9l1 --> F9s5 end %% ForEachUERolleInEmneUERollerList subgraph F10["ForEachUERolleInEmneUERollerList-subFLOW"] direction TB F10s1["Set UE context"]:::step F10s2["Clear role variables"]:::step F10s3["Set course_id and SIS ids"]:::step F10d1{Role code known?}:::decision F10s4["Add role to emneUEUARollerList"]:::step F10s1 --> F10s2 --> F10s3 --> F10d1 -->|"yes"| F10s4 end %% ForEachUERolleInUERollerList subgraph F11["ForEachUERolleInUERollerList-subFLOW"] direction TB F11s1["Set UE and emne context"]:::step F11s2["Clear role variables"]:::step F11s3["Set course_id and SIS ids"]:::step F11d1{Role code known?}:::decision F11s4["Add role to UEFagRollerList"]:::step F11s1 --> F11s2 --> F11s3 --> F11d1 -->|"yes"| F11s4 end %% ForEachUARolleInUARollerList subgraph F12["ForEachUARolleInUARollerList-subFLOW"] direction TB F12s1["Set UA and UE context"]:::step F12s2["Clear role variables"]:::step F12s3["Set course_id and SIS ids"]:::step F12d1{Role code known?}:::decision F12s4["Add role to UAFagRollerList"]:::step F12s1 --> F12s2 --> F12s3 --> F12d1 -->|"yes"| F12s4 end %% Cross-flow links F0s4 --> F1s1 F1l1 --> F2s1 F2l2 --> F3s1 F3s9 --> F5s1 F3s11 --> F5s1 F3s15 --> F7s1 F4l1 --> F5s1 F6l1 --> F5s1 F7s10 --> F8s1 F7s11 --> F9s1 F7s17 --> F10s1 F7s22 --> F11s1 F7s30 --> F12s1 F2s1 --> F13 F2s9 --> F14 F6s3 --> F14 F7s3 --> F14 F7s13 --> F14 F7s18 --> F14 F7s23 --> F14 F7s25 --> F14 F8s2 --> F14 F3s5 --> F15 F4s6 --> F16 F4s6 --> F16 F5s5 --> F16 F4s8 --> F17 F5s10 --> F17 F4s10 --> F18 F4s12 --> F18 F5s8 --> F18 F5s12 --> F18 F6s9 --> F18 F7s32 --> F19 F19s3 --> F20 F19s15 --> F21 F19s22 --> F22 F19s30 --> F23 F23s3 --> F24 F18s3 --> F24 F23s7 --> F25 F23s11 --> F26 F19s15 --> F21 F23s16 --> F21 F2s17 --> F27 F16s3 --> F28 F3s5 --> Canvas F15s3 --> Canvas F20s3 --> Canvas F21s4 --> Canvas F24s3 --> Canvas F25s3 --> Canvas F26s3 --> Canvas F28s2 --> Canvas %% Legend subgraph Legend direction TB L1["Step"]:::step L2[["External sub-flow call"]]:::ext L3{"Decision"}:::decision L4(("Loop")):::loop L5["Note"]:::note end subgraph F13[extractOrgInfo.FSAccessInfo.CanvasAccessInfo-subFLOW] B[Set Variable: orgInfo = payload] --> C[Set Variable: orgShortName = orgInfo.ShortName] C --> D[Set Variable: orgFSID = orgInfo.FsId] D --> E[Log Info: Running for orgShortName] E --> F[Transform: FSAccessInfo] F --> G[Set Variable: fsUserName from orgInfo.FSGraphQL] G --> H[Set Variable: fsURL from orgInfo.FSGraphQL] H --> I[HTTP GET: CanvasAccessToken] I --> J[Transform: CanvasAccessInfo] J --> K[Set Variable: canvasOAuthAccessToken] K --> L[Set Variable: canvasInstitutionSubdomain] end subgraph F14[fsQueryExecution-subFLOW] class F14 blue F14s1([Start]) --> F14s2[Try: Begin] F14s2 --> F14s3[Until Successful] F14s3 --> F14s4[Do FS Query using FSQueryExpression and FSQueryVariables] F14s4 --> F14s5[Validate: Response is not null] F14s5 --> F14s6[Validate: No errors in payload] F14s6 --> F14s7([Success]) F14s2 -.-> F14s8{Error Occurs?} F14s8 -->|Yes| F14s9[On Error Propagate] F14s9 --> F14s10[Log: Error Message] F14s10 --> F14s11[Log: Payload] F14s11 --> F14s12[Log: FSQueryVariables] F14s12 --> F14s13[Log: FSQueryExpression] F14s13 --> F14s14([End Error]) F14s8 -->|No| F14s7 F14s7 --> F14s15([End]) end subgraph F15[getCanvasAccount-by-sis_account_id-subFLOW] F15s1([Start]) --> F15s2[Try: Begin] F15s2 --> F15s3[Set Variable: canvasGetAccountResponse = null] F15s3 --> F15s4[Canvas API: Get account by sis_account_id] F15s4 --> F15s5[Set Payload: canvasGetAccountResponse] F15s5 --> F15s6{Canvas Error?} F15s6 -->|Not Found| F15s7[On Error Continue: Set Payload = null] F15s7 --> F15s10([End Success]) F15s6 -->|Error Other Than Not Found| F15s8[On Error Propagate] F15s8 --> F15s9[Log: Unexpected Canvas Error with sis_account_id] F15s9 --> F15s11([End Error]) F15s6 -->|No Error| F15s10 F15s10 --> F15s12([End]) end subgraph F16["createUpdateEnrollmentTerm-subFLOW"] F16s1["Logger: Flow started"] F16s2["Transform: Nullify enrollment term responses"] F16s3["Flow Ref: getEnrollmentTerm-by-sis_term_id-subFLOW"] F16s4["Set Variable: enrollmentTermNotExistInLMS"] F16s5{"Enrollment term exists in LMS?"} F16s6["Logger: enrollmentTermNotExistInLMS"] F16s7["Flow Ref: createNewEnrollmentTerm-subFLOW"] F16s8["Logger: enrollmentTermExistInLMS, updating it..."] F16s9["Set Variable: enrollment_term_id"] F16s10["Flow Ref: updateEnrollmentTerm-subFlow"] F16s1 --> F16s2 --> F16s3 --> F16s4 --> F16s5 F16s5 -- Yes --> F16s6 --> F16s7 F16s5 -- No --> F16s8 --> F16s9 --> F16s10 end subgraph F17["createUpdateCourse-subFLOW"] F17s1["Logger: Flow started"] F17s2["Transform: Nullify course responses"] F17s3["Flow Ref: getCourse-by-sis_course_id-subFLOW"] F17s4["Set Variable: courseNotExistInLMS"] F17s5{"Course exists in LMS?"} F17s6["Logger: courseNotExistInLMS"] F17s7["Flow Ref: createNewCourse-subFLOW"] F17s8["Logger: courseExistInLMS, updating it..."] F17s9["Set Variable: course_id"] F17s10["Set Variable: course_code"] F17s11["Set Variable: course_name"] F17s12["Set Variable: enrollment_term_id"] F17s13["Flow Ref: updateExistingCourse-subFlow"] F17s1 --> F17s2 --> F17s3 --> F17s4 --> F17s5 F17s5 -- Yes --> F17s6 --> F17s7 F17s5 -- No --> F17s8 --> F17s9 --> F17s10 --> F17s11 --> F17s12 --> F17s13 end subgraph F18["createUpdateSection-subFLOW"] F18s1["Logger: Flow started"] F18s2["Transform: Nullify section responses"] F18s3["Flow Ref: getSection-by-sis_section_id-subFLOW"] F18s4["Set Variable: sectionNotExistInLMS"] F18s5{"Section exists in LMS?"} F18s6["Logger: sectionNotExistInLMS"] F18s7["Flow Ref: createNewSection-subFLOW"] F18s8{"Is Manual Section?"} F18s9["Logger: Manual section exists"] F18s10["Logger: sectionExistInLMS, updating it..."] F18s11["Set Variable: section_id"] F18s12["Set Variable: section_name"] F18s13["Flow Ref: updateExistingSection-subFlow"] F18s1 --> F18s2 --> F18s3 --> F18s4 --> F18s5 F18s5 -- Yes --> F18s6 --> F18s7 F18s5 -- No --> F18s8 F18s8 -- Yes --> F18s9 F18s8 -- No --> F18s10 --> F18s11 --> F18s12 --> F18s13 end subgraph F19["deleteCreateEnrollment-subFLOW"] class F19 ex F19s1(["Set sis_section_id"]):::step F19s2{"Try getting users in section"}:::decision F19s3(["Flow Ref: getUsersInSection-by_sis_section_id-subFLOW"]):::ext F19s4(["Set sectionNotExist = false"]):::step %% Error handling F19s5{"Error: Section doesn't exist?"}:::decision F19s6(["Set sectionNotExist = true"]):::step F19s7(["Log unexpected Canvas error"]):::step F19s8(["Set listOfSisUserIDsInSectionFromCanvas"]):::step F19s9(["Set ListOfUsers2bDeletedInCanvasSection"]):::step F19s10(["Log array size for delete list"]):::ext %% Loop for deletions F19s11{{"For Each user in DeleteList"}}:::loop F19s12(["Set canvasUser2bDeleted"]):::step F19s13(["Set sis_course_id"]):::step F19s14(["Set enrollment_id"]):::step F19s15(["Flow Ref: DeleteUserInSection-subFLOW"]):::ext F19s16(["Set ListOfUsers2bCreatedInCanvasSection"]):::step F19s17(["Log array size for create list"]):::ext %% Loop for creations F19s18{{"For Each user in CreateList"}}:::loop F19s19(["Set canvasUser2bCreated"]):::step F19s20(["Set enrollment_id"]):::step F19s21(["Set payload: personProfil = user"]):::step F19s22(["Flow Ref: Create/Update User"]):::ext F19s23(["Reset course_id"]):::step F19s24(["Set sis_section_id"]):::step F19s25(["Set erAktivRolle"]):::step F19s26(["Set fsRolleKode"]):::step F19s27(["Set enrollmentType"]):::step F19s28(["Set role_id"]):::step F19s29(["Set limit_privilage = false"]):::step F19s30(["Flow Ref: createSkipDeleteUserEnrollmentInSection-subFLOW"]):::ext F19s1 --> F19s2 F19s2 --> F19s3 --> F19s4 --> F19s8 F19s2 --> F19s5 F19s5 -- Yes --> F19s6 --> F19s8 F19s5 -- No --> F19s7 --> F19s8 F19s8 --> F19s9 --> F19s10 --> F19s11 F19s11 --> F19s12 --> F19s13 --> F19s14 --> F19s15 --> F19s11 F19s11 --> F19s16 --> F19s17 --> F19s18 F19s18 --> F19s19 --> F19s20 --> F19s21 --> F19s22 --> F19s23 --> F19s24 --> F19s25 --> F19s26 --> F19s27 --> F19s28 --> F19s29 --> F19s30 --> F19s18 %% Class assignments class F19s1,F19s4,F19s8,F19s9,F19s12,F19s13,F19s14,F19s16,F19s19,F19s20,F19s21,F19s23,F19s24,F19s25,F19s26,F19s27,F19s28,F19s29 step class F19s3,F19s10,F19s15,F19s17,F19s22,F19s30 ext class F19s2,F19s5,F19s11,F19s18 decision class F19s7,note note class F19s6,F19s10,F19s17 loop end subgraph F20["getUsersInSection-by_sis_section_id-subFLOW"] F20s1["Try"] F20s2["Set Variable: nullify canvasGetUsersInSectionResponse"] F20s3["Canvas API: Get users in section by sis_section_id"] F20s4["Set Payload: canvasGetUsersInSectionResponse"] F20s5{"Error?"} F20s6["Logger: Error received from Canvas"] F20s1 --> F20s2 --> F20s3 --> F20s4 --> F20s5 F20s5 -- Yes --> F20s6 F20s5 -- No --> F20s4 end subgraph F21["DeleteUserInSection-subFLOW"] class F21 ex F21s1(["Logger: Flow has been started"]):::step F21s2{"Try"}:::decision F21s3(["Set Variable: nullify canvasUpdateEnrollUserResponse"]):::step F21s4(["Canvas API: Delete enrollment in course/section"]):::ext F21s5{"Error occurred?"}:::decision F21s6(["Logger: Cannot delete user enrollment"]):::step F21s1 --> F21s2 F21s2 --> F21s3 --> F21s4 --> F21s5 F21s5 -- Yes --> F21s6 F21s5 -- No --> F21s2 %% Class assignments class F21s1,F21s3,F21s6 step class F21s4 ext class F21s2,F21s5 decision end subgraph F22["forEachUserInUserListCreateUpdateUser-subFLOW"] class F22 ex F22s1(["Set variable: user = payload"]):::step F22s2(["Set variable: name_of_user = fornavn + etternavn"]):::step F22s3(["Set variable: brukernavn = user.feideBruker before @"]):::step F22s4(["Set variable: unique_id = brukernavn@domain + sis_id_temp_DEV_only"]):::step F22s5(["Set variable: sis_user_id = fs:orgFSID:personlopenummer + sis_id_temp_DEV_only"]):::step F22s6(["Set variable: user_email_address"]):::step F22s7(["Set variable: account_id = 1"]):::step F22s8(["Flow Ref: createUpdateUser-subFLOW"]):::ext F22s9(["Set variable: user_id from Canvas responses"]):::step F22s10(["Set variable: FSUserID = user.id"]):::step %% Connections F22s1 --> F22s2 --> F22s3 --> F22s4 --> F22s5 --> F22s6 --> F22s7 --> F22s8 --> F22s9 --> F22s10 %% Class assignments class F22s1,F22s2,F22s3,F22s4,F22s5,F22s6,F22s7,F22s9,F22s10 step class F22s8 ext end subgraph F23["createSkipDeleteUserEnrollmentInSection-subFLOW"] class F23 ex F23s1(["Logger: subFlow has been started"]):::step F23s2(["Transform: nullify canvasGetSectionResponse / canvasEnrollUser2SectionResponse / canvasGetUserEnrollmentsResponse"]):::step F23s3(["Flow Ref: getSection-by-sis_section_id-subFLOW"]):::ext F23s4(["Set variable: sectionNotExistInLMS = isEmpty(payload)"]):::step F23s5{"Section exists in LMS?"}:::decision %% When section does not exist F23s6(["Logger: Section does not exist. Skipping user enrollment"]):::step %% When section exists F23s7(["Flow Ref: getUserEnrollment-by-user_id-subFLOW"]):::ext F23s8(["Set variable: userEnrollmentNotExistInLMS = isEmpty(payload)"]):::step F23s9{"Create new or update existing user enrollment?"}:::decision F23s10(["Logger: Enrolling user"]):::step F23s11(["Flow Ref: EnrollUser2Section-subFLOW"]):::ext F23s12(["Logger: Deleting user enrollment"]):::step F23s13(["Validation: userEnrollmentList has only one entry?"]):::step F23s14(["Set variable: sis_course_id"]):::step F23s15(["Set variable: enrollment_id"]):::step F23s16(["Flow Ref: DeleteUserInSection-subFLOW"]):::ext F23s17(["Logger: Skip updating or enrolling user"]):::step %% Connections F23s1 --> F23s2 --> F23s3 --> F23s4 --> F23s5 F23s5 -- No (section not exist) --> F23s6 F23s5 -- Yes (section exists) --> F23s7 --> F23s8 --> F23s9 F23s9 -- userEnrollmentNotExistInLMS && erAktivRolle --> F23s10 --> F23s11 F23s9 -- !userEnrollmentNotExistInLMS && !erAktivRolle --> F23s12 --> F23s13 --> F23s14 --> F23s15 --> F23s16 F23s9 -- otherwise --> F23s17 %% Class assignments class F23s1,F23s2,F23s4,F23s6,F23s8,F23s10,F23s12,F23s13,F23s14,F23s15,F23s17 step class F23s3,F23s7,F23s11,F23s16 ext class F23s5,F23s9 decision end subgraph F24["getSection-by-sis_section_id-subFLOW"] class F24 ex F24s1{"Try"}:::decision F24s2(["Set variable: canvasGetSectionResponse = null"]):::step F24s3(["Canvas API: Get section by sis_section_id"]):::ext F24s4(["Set payload = canvasGetSectionResponse"]):::step F24s5{"Error occurred?"}:::decision F24s6(["Set payload = null (resource does not exist)"]):::step F24s7(["Logger: Unexpected error from Canvas"]):::step F24s1 --> F24s2 --> F24s3 --> F24s4 --> F24s5 F24s5 -- Resource does not exist --> F24s6 F24s5 -- Other error --> F24s7 %% Class assignments class F24s1,F24s5 decision class F24s2,F24s4,F24s6,F24s7 step class F24s3 ext end subgraph F25["getUserEnrollment-by-user_id-subFLOW"] class F25 ex F25s1{"Try"}:::decision F25s2(["Set variable: canvasGetUserEnrollmentResponse = null"]):::step F25s3(["Until Successful: Canvas API Get user enrollments by user_id or sis_user_id"]):::ext F25s4(["Transform: Filter userEnrollmentList from canvasGetUserEnrollmentsResponse"]):::step F25s5(["Validation: Is userEnrollmentList empty = false?"]):::step F25s6(["Set payload = canvasGetUserEnrollmentsResponse"]):::step F25s7{"Error occurred?"}:::decision F25s8(["Set payload = null if Value expected false error"]):::step F25s9(["Logger: Unexpected error from Canvas"]):::step %% Connections F25s1 --> F25s2 --> F25s3 --> F25s4 --> F25s5 --> F25s6 F25s1 --> F25s7 F25s7 -- Expected error --> F25s8 F25s7 -- Other error --> F25s9 %% Class assignments class F25s1,F25s7 decision class F25s2,F25s4,F25s5,F25s6,F25s8,F25s9 step class F25s3 ext end subgraph F26["EnrollUser2Section-subFLOW"] class F26 ex F26s1{"Try"}:::decision F26s2(["Set variable: canvasEnrollUser2SectionResponse = null"]):::step F26s3(["Enroll user to section (Canvas API)"]):::ext F26s4{"Error occurred?"}:::decision F26s5(["Logger: Cannot enroll user to this section (expected 'resource does not exist' error)"]):::step F26s6(["Logger: Unexpected error (on-error-propagate)"]):::step %% Connections F26s1 --> F26s2 --> F26s3 --> F26s4 F26s4 -- Resource does not exist --> F26s5 F26s4 -- Other errors --> F26s6 %% Class assignments class F26s1,F26s4 decision class F26s2,F26s3,F26s5,F26s6 step class F26s3 ext end subgraph F27[auditLogRegister-subFLOW] F27s1([Start]):::flow --> F27s2[Log: Started to register auditInfoMap in OAI-DB]:::step F27s2 --> F27s3[Set Variable: OAIDBkey = auditInfoMap.key]:::step %% Try block for DB registration F27s3 --> F27s4[Try: Register audit log in DB]:::flow F27s4 --> F27s5[Add audit log to DB]:::ext F27s5 --> F27s6{DB Error?}:::decision %% Error handling for DB registration F27s6 -->|Yes| F27s7[Log ERROR: Cannot register auditInfo in DB]:::step F27s7 --> F27s8([Continue]):::step F27s6 -->|No| F27s9([DB Registration OK]):::step %% Decision: audit successful? F27s9 --> F27s10{auditInfoMap.success == false?}:::decision %% If not successful F27s10 -->|Yes| F27s11[Log: Caught Error, sending Slack warning]:::step F27s11 --> F27s12[Slack: Report error with auditInfoMap]:::ext F27s12 --> F27s13([Continue]):::step %% If successful F27s10 -->|No| F27s14[Log: Audit registered successfully]:::step %% Second Try - send to Humio F27s14 --> F27s15[Try: Send auditInfo to Humio]:::flow F27s15 --> F27s16[Send auditInfo to Humio]:::ext F27s16 --> F27s17{Humio Error?}:::decision %% Humio error handling F27s17 -->|Yes| F27s18[Log ERROR: Cannot send auditInfo to Humio]:::step F27s18 --> F27s19([Continue]):::step F27s17 -->|No| F27s20([Humio Send OK]):::step F27s20 --> F27s21([End]):::flow end subgraph F28["F28 - getEnrollmentTerm-by-term_id-subFLOW"] F28s1["Nullify canvasGetEnrollmentTermResponse"]:::step F28s2["Call Canvas API: Get Enrollment Term by term_id"]:::ext F28s3["Set payload = canvasGetEnrollmentTermResponse"]:::step %% Error Handling F28s4{"Error contains 'The specified resource does not exist.'?"}:::decision F28s5["Set payload = null (continue error)"]:::step F28s6["Log error and propagate"]:::step %% Flow connections F28s1 --> F28s2 --> F28s3 --> F28s4 F28s4 -->|Yes| F28s5 F28s4 -->|No| F28s6 end Canvas(("Call to Canvas")):::red

Brukte spørringer mot FS (Graph)

query organisasjonsenheter($inst: String!) { organisasjonsenheter( first: 1000 filter: {eierInstitusjonsnummer: $inst, institusjonummer: $inst, skalEksporteresTilLms: true, erAktiv: true} ) { nodes { instituttnummer fakultet { navn { nob eng nno } fakultetsnummer } lmsRomkode navnAlleSprak { nob nno eng } gruppenummer organisasjon { organisasjonskode navn { nob eng nno } } } } }
query emneHierarki($inst: String!, $aar: Int!, $termin: EmneIkkeUtloptITerminTerminbetegnelse!) { emner( filter: {eierInstitusjonsnummer: $inst, ikkeUtloptITermin: {arstall: $aar, terminbetegnelse: $termin}} first: 1000 after: "MjA6MTIzNCwxMjM0LExNUyBURVNURU1ORSwx" ) { ...QueryEmneConnectionFragment } } fragment QueryEmneConnectionFragment on QueryEmneConnection { nodes { id kode versjonskode navnAlleSprak { nob eng nno } sprakvalg { tilbysSomUndervisningssprak sprak { iso6392Kode } } organisasjonsenhet { studieAnsvarlig { navnAlleSprak { nob } fakultet { navn { nob } fakultetsnummer } organisasjon { organisasjonskode navn { nob eng nno } } gruppenummer instituttnummer } } } pageInfo { endCursor hasNextPage } }
query studieprogramKullKlassHierarki($inst: String!) { studieprogram( filter: {eierInstitusjonsnummer: $inst, skalEksporteresTilLms: true, erAktiv: true} first: 1000 ) { nodes { kode lmsRomkode personroller { gyldighetsperiode { fraDato tilDato } fsRolle { imsRolle kode navn { eng lmsNavn nno nob } erAktiv } aktiv } organisasjonsenhet { studieansvarlig { lmsRomkode instituttnummer organisasjon { organisasjonskode navn { nob eng nno } } gruppenummer fakultet { fakultetsnummer navn { eng nno nob } } navnAlleSprak { eng nno nob } skalEksporteresTilLms erAktiv } } navnAlleSprak { eng nno nob } kull(aktiv: true) { lmsRomkode navnAlleSprak { en no } skalEksporteresTilLms termin { arstall betegnelse { kode navnAlleSprak { en nb nn } } } tjenestenummerForLms erAktiv klasser(erAktiv: true, skalEksporteresTilLms: true) { kode lmsRomkode skalEksporteresTilLms erAktiv navnAlleSprak { und } } } skalEksporteresTilLms skalPubliseres } pageInfo { endCursor hasNextPage startCursor } totalCount } }
query EtterutdanningsKursAktivitet($inst: String!) { evuKurs( filter: {eierInstitusjonsnummer: $inst, skalEksporteresTilLms: true, erAktiv: true} first: 1000 ) { nodes { kurskode skalEksporteresTilLms tidsangivelseskode erAktiv navnAlleSprak { eng nno nob } kursperiode { fraDato tilDato } tjenestenummerForLms lmsRomkode administrativtAnsvarligEnhet { erAktiv skalEksporteresTilLms navnAlleSprak { eng nno nob } lmsRomkode instituttnummer gruppenummer fakultet { navn { eng nno nob } fakultetsnummer } organisasjon { organisasjonskode navn { nob eng nno } } } } pageInfo { endCursor hasNextPage } } }
query emneHierarki($inst: String!, $aar: Int!, $termin: EmneIkkeUtloptITerminTerminbetegnelse!) { emner( filter: {eierInstitusjonsnummer: $inst, ikkeUtloptITermin: {arstall: $aar, terminbetegnelse: $termin}} first: 1000 after: "MjA6MTIzNCwxMjM0LExNUyBURVNURU1ORSwx" ) { ...QueryEmneConnectionFragment } } fragment QueryEmneConnectionFragment on QueryEmneConnection { nodes { id kode versjonskode navnAlleSprak { nob eng nno } sprakvalg { tilbysSomUndervisningssprak sprak { iso6392Kode } } tjenestenummerForLms undervisningsenheter(filter: {skalEksporteresTilLms: true}) { lmsRomkode terminnummer termin { arstall betegnelse { kode } } undervisningsaktiviteter { kode navnAlleSprak { nob nno eng } terminnummer skalEksporteresTilLms lmsRomkode } undervisningenFolgesAv { kode navnAlleSprak { nob nno eng } } undervisningenFolger { kode navnAlleSprak { nob eng nno } } } organisasjonsenhet { studieAnsvarlig { gruppenummer instituttnummer fakultet { fakultetsnummer navn { eng nno nob } } organisasjon { organisasjonskode } skalEksporteresTilLms navnAlleSprak { eng nno nob } } } } pageInfo { endCursor hasNextPage } }
query singleEmne($inst: String!, $emneKode: [String!]) { emner(filter: {eierInstitusjonsnummer: $inst, emnekoder: $emneKode}) { nodes { kode versjonskode tjenestenummerForLms undervisningsenheter { terminnummer termin { arstall betegnelse { kode navnAlleSprak { en nb } } } } } } }

Feilhåndtering, konsekvenser av feil og overordnet risikoanalyse

Generelt vil status og dermed eventuelle feil være synlig og tilgjengelig for institusjonen via logg-oversikten. Det er også utarbeidet mulighet for at enkelt personer ved institusjonen kan melde seg på for mottak av feilmeldinger på epost daglig.

Videre har vi overvåkning av loggene via Humio for å fange opp feil-situasjoner utenfor institusjonens virkeområde, som f.eks. utilgjengelige API endepunkter og bugs i koden.

 

  • Hva skjer ved overload i kø?

    • Det skjer ikke. Men om vi skulle på grunn av noe feil ikke motta meldingene fra eksamensystemene, vil : Inspera vite om det og sende dem på nytt. Wiseflow: er basert på polling, så vi kan alltid polle igjen

  • Hva skjer med ufullstendige meldinger?

    • De vil feile og det vil vi oppdage i loggene og kan ta aksjon basert på det.

  • Inneholder meldingene personopplysninger?

    • Merk at det utveksles mange “meldinger” mellom ulike systemer i løpet av en integrasjon. Meldinger fra eksamensystemene inneholder ikke noe persondata, men data vi henter fra FS basert på de meldingene gjør. Filene vi arkiverer (som kommer fra eksamensystemene) kan også potensielt inneholde personopplysninger.

  • Noe om viktige feil/situasjoner som må passes ekstra på (som kan ha stor konsekvens) :

    • Oppgavene ikke arkiveres (miste meldinger)

    • Oppgaver arkiveres med feil info. (feiltolking eller feil logikk)

    • Oppgaver arkiveres med feil tilganger (slik at oppgaver som ikke skal være offentlige, arkiveres slik at de blir offentlig synlige) 

 

Nødvendige tilganger

FS Graph:

  • ? (Avventer fra FS)

Kommentarer